Найдите мертвый код в программе C с помощью анализа во время выполнения

У меня есть большое приложение, написанное на C + POSIX, внутри него много функций, которые никогда не вызываются. Однако из-за размера кода их сложно отследить вручную.
Некоторые люди предлагали использовать gcc с -Wunusedи lto, но он не вернул ни одной используемой функции, в то время как я продолжаю находить и удалять некоторые вручную.

Поэтому я думаю, что мне нужен инструмент покрытия кода для анализа программы во время выполнения. Люди предложили мне gcov или valgrind, но я не смог найти, как их использовать для печати списка мертвых функций. Только gcov показал, что используются только 68% скомпилированных функций, но я не могу их перечислить.

Итак, кто-нибудь знает хороший инструмент, и если да, то скажите мне, как именно я могу использовать его для этой цели (приветствуется пример командной строки) ?

Я удалил все функции, которые не используются в исходном коде. В исходном коде остаются только такие функции:

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

Where conditional statementникогда не бывает истинным во время выполнения, и удаление dead_function()приведет к удалению оператора, чтобы избежать неопределенных ошибок.

Итак, то, что вы ищете, на самом деле не неиспользуемые функции ( dead_function используется ), а мертвый код. Для этого нужны совсем другие техники! Имейте в виду, что анализ во время выполнения найдет только тот код, который не был запущен в конкретном выполнении программы — этот код может быть активен при различных обстоятельствах.
@Gilles Жиль: это своего рода проблема gcov ... Как объединить результаты разных запусков? У меня для параметра. Это говорит о том, что программа делает только одну вещь: находит очень большие простые числа, поэтому легко проверить все случаи. Именно поэтому я пытаюсь удалить мертвый код (критичный для производительности) .
Похоже, вы не хотите удалять мертвые функции , а скорее удаляете мертвые условные коды . Имею ли я это право?
@IraBaxter: Нет, потому что в настоящее время меня не волнуют неиспользуемые операторы внутри функций. Я хочу удалить только те функции, которые совершенно бесполезны (например, те, что связаны с поплавками) .
Имейте в виду, что код, который никогда не вызывается, очень мало влияет на производительность, за исключением времени загрузки и объема памяти. Чтобы помочь в обоих случаях, переместите как можно больше своего кода во множество небольших DLL или общих библиотек .so - те, которые фактически никогда не используются, никогда не будут загружены. Если у вас серьезные проблемы с производительностью, возможно, у вас есть код, который вызывается, но результаты никогда не используются — lint может помочь найти их.
Хотя я согласен с тем, что неприглядно иметь неиспользуемый код (включая код, закомментированный), вы можете быть спокойны, зная, что компоновщик должен позаботиться об этом за вас. Если вы найдете инструмент, убедитесь, что он правильно обрабатывает указатели на функции. Например, в моей работе мы полагаемся на en.wikipedia.org/wiki/Finite-state_machine , которые управляются en.wikipedia.org/wiki/Branch_table , поэтому наши самые важные функции никогда не вызываются напрямую, а только косвенно. То же самое может произойти с обратными вызовами.
@Mawg: компоновщик может сделать это только в том случае, если нет вызова из других функций. Но в моем случае вызовы есть, но они никогда не используются при запуске программы. Я понимаю, что это связано с удалением мертвого условного кода.
@SteveBarnes: Код, который никогда не вызывается для небольшой программы, оказывает влияние из-за кешей ЦП. Использование общих библиотек будет работать только для функций, которые не имеют связанных вызовов внутри программы, а не для тех, которые никогда не вызываются во время выполнения. То же самое с решением компоновщика.
Единственное известное мне «бесплатное» или почти бесплатное решение для случая использования, когда функция «используется» кодом более высокого уровня, но никогда не вызывается в действительности, заключается в следующем: а) Напишите несколько тестов, которые, как вы уверены, охватывают все варианты использования б) в каждой функции добавьте printf(__func__);в качестве первой строки, в gcc это будет печатать имя функции каждый раз, когда она вызывается - запустите свой тестовый захват вывода, тогда любое имя, отсутствующее в выводе, не вызывается.
@SteveBarnes: Было много функций, которые я хотел бы сделать автоматически, как это уже делает gcov. Может быть, я просто забыл возможность перечислить их, но я не могу ее найти.
Используйте файл карты, чтобы получить список всех функций, которые есть в коде, и gcov, чтобы перечислить все те, которые вызываются, вычтите это из первого списка, и вы получите список функций, которые не вызывались. Короткий скрипт на Python или немного магии awk должны помочь.
Конечно, если вы используете gcovr, gcovr.com/guide.html , вы сможете быстро идентифицировать строки кода, которые никогда не выполняются.
@SteveBarnes: я неправильно следовал инструкциям и не могу найти соответствующую опцию, которая позволила бы перечислить функции, которые не являются частью 68%, используемых в выводе gcov.
@ user2284570 - посмотрите мой новый ответ.
Независимо от того, какое решение, будут проблемы, если используются таблицы переходов (указатели функций)
@Mawg Я не использую таблицы переходов напрямую. Возможно, компилятор может генерировать некоторые автоматически, но я ими не пользуюсь.
Не многие. Мы часто используем их во встраиваемых системах для связи между состояниями и четностями. Остерегайтесь также функций обратного вызова.

Ответы (3)

Учитывая, что @user2284570 находится в комфортной ситуации, охватывая 100% вариантов использования с помощью тестов, динамический анализ кода даст ответ. В других случаях удаление функций, их вызовов и условий потребует тщательной проверки.

Любой инструмент покрытия кода будет сообщать о покрытии функций в той или иной форме. Основная проблема заключается в сообщении о точном местоположении неиспользуемых (здесь: мертвых) функций. Я не могу говорить о других инструментах, но у нашей компании есть опция отчетности в текстовом формате, в которой есть заполнители для имени исходного файла и данных строки. Поскольку был запрошен конкретный пример командной строки, вот один из них:

$ 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'

Это напечатает местоположения мертвых функций, например:

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

Студент местного университета написал более подробные инструкции для этого подхода.

После удаления неиспользуемых функций вы также захотите проанализировать покрытие ветвей и удалить лишние операторы if() и другие. Просто остерегайтесь побочных эффектов оцениваемых выражений. Но, к счастью, ваше идеальное покрытие кода обнаружит регрессии.

Должен сказать, что с тех пор я использовал пошаговую ручную отладку для удаления мертвого кода. Так что я не буду пробовать ваш инструмент.

Вы можете использовать splintс alluseфлагом для проверки неиспользуемых функций, но лично я бы использовал doxygenдля создания карты вызовов - любые функции, у которых нет родителей, вероятно, не используются - просто ищите любые функции, которые находятся в таблицах функций, которые могут не вызываться напрямую но такие вещи, как конечные автоматы, могут вызываться из индекса таблицы.

Doxygen — бесценный инструмент для работы с большими базами кода, и в любом случае стоит научиться его использовать, он бесплатный и доступен для нескольких платформ, а также поощряет документирование вашего кода по ходу работы.

В случае кода, который вызывается, но только из недостижимого кода, вам придется использовать инструмент полного статического анализа, такой как LDRA (дорогостоящий), который укажет вам на недоступный код. В этом случае лучше сначала удалить весь недостижимый код, а затем искать неиспользуемые функции. В качестве альтернативы вам понадобится набор тестов, который, как вы уверены, реализует 100% функциональности - тогда вы можете использовать профилировщик или инструмент покрытия, такой как gcov, в своей программе во время запуска набора тестов. Если ваш тест задействовал все ваши функциональные возможности и у вас есть части с 0% покрытием, то они не вызываются, но тогда вам придется найти недоступные для них вызовы и удалить этот код, чтобы компоновщик все равно не жаловался.

" Вы можете использовать шину с флагом alluse " Я до сих пор не понимаю... можете ли вы привести пример командной строки? Для doxygen ответ - нет! Я только что закончил удалять все функции, которые не использовались в коде. Остаются только те, которые используются в коде, но где некоторые условные операторы заставляют их никогда не вызываться во время выполнения. и карта вызовов не может помочь с сотнями функций. Мне определенно нужен читаемый вывод с отсортированными невызванными функциями и с именами файлов и номерами строк области, в которой они объявлены.
Стали нет подробного примера... Вот и не понимаю, что можно сделать...
@user2284570 user2284570 Этот ответ верен для вопроса, который вы изначально разместили. Вы не должны изменять вопросы таким образом, чтобы сделать недействительными ответы! Было бы лучше, если бы вы вернули свое редактирование «Обновить» к вопросу и вместо этого задали новый вопрос.
Остерегайтесь, конечно, функций, которые вызываются указателем функции, таких как обратные вызовы и таблицы переходов состояния/события.
@Mawg таких функций не было.

Чтобы ответить на ваш пересмотренный вопрос - если вы скомпилируете весь свой код с gcc -fprofile-arcs -ftest-coverageустановленными параметрами, а затем запустите набор тестов, которые, как вы уверены, охватывают все функциональные возможности и все возможности (возможно, в нескольких прогонах).

Утилита gcovтребует, чтобы вы выполнили часть работы — у нее не просто есть опция «скажите мне, что не было вызвано», поэтому вам придется найти те функции, которые не были вызваны.

Вы можете использовать gcovс --function-summariesопцией для каждого исходного файла, который вы создадите набор выходных файлов, которые будут включать сводки функций - ищите любые из тех, которые включают neverили 0%, чтобы найти невыполненные функции.

Я бы предложил либо добавить функцию, которая, как вы знаете, никогда не будет вызываться, либо узнать о той, которую вы еще не удалили - это позволит вам увидеть, как выглядит вывод - вы можете затем использовать grep, чтобы найти их все.

Следующим шагом будет использование grepили что-то подобное для просмотра gcovвывода для всех мест, где эти функции присутствуют в вашем коде - вы должны увидеть счетчики выполнения 0 для всей ветки, содержащей вызов , это даст вам хорошую отправную точку либо для расширения ваших тестов — для вариантов использования, которые вы пропустили, — либо для удаления кода.

Затем он точно не может сделать то, что мне нужно: отсортировать неиспользуемую функцию и указать строки по путям к файлам. То же самое я сделал с kcachegrind перед публикацией этого вопроса. Ручная сортировка функций и поиск их в поиске - это нормально, но это не так, если у вас их много.
Я думаю, вам придется нанять кого-то, кто может программировать , чтобы сделать это тогда! Инструменты мало что сделают за вас, иначе вы будете лишними.
Мне не нужно что-то для автоматического удаления. Мне нужно что-то, что может отображать неиспользуемые функции во время выполнения без используемых функций в этом списке. Мне также нужен их путь объявления. Также я делаю это для себя. Так что ожидайте, что я не буду писать свой собственный ответ в течение нескольких лет.
Steel не может автоматически отсортировать неиспользуемые функции из вызванных… Тем временем я осознал вычислительные требования для генерации безопасных случайных простых чисел, подходящих для 65Ko.