Мотивация
Периодически встречающимися проблемами кода на С и C++ являются утечки памяти и неопределенное поведение. Даже если вы используете умные указатели, то от ошибок в библиотеках сторонних разработчиков вы не застрахованы. Для поиска ошибок в коде существуют специальные инструменты:
санитайзеры;
valgrind.
Первые практически не влияют на быстродействие программы, но требуют перекомпиляции программы. Второй удобен тем, что программу можно запустить без повторной сборки, но сильно снижает скорость работы кода, что особенно заметно при анализе больших приложений.
Независимо от выбора инструмента анализа может возникнуть ситуация, что вы обнаружили ошибку в сторонней библиотеке (например системной). Т.к. анализаторы получают информацию об ошибке при каждом вызове не корректно работающей функции, то логи очень быстро будут заполнены тысячами строк однообразной информации. Однако, санитайзеры и valgrind можно настроить так, что в лог будут попадать только ошибки связанные с интересующим вас библиотеками.
Понадобится следующее программное обеспечение:
свежий дистрибутив linux
gcc или clang
valgrind
llvm
Qt5 (для эксперимента)
Исходный код программы
Тестируемая программа будет отображать на экране пустое окно QMainWindow.
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(cmake-qt-widgets LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(QT_COMPONENTS
Core
Widgets)
find_package( Qt5 REQUIRED COMPONENTS
${QT_COMPONENTS}
)
find_package(Threads)
add_executable(${PROJECT_NAME}
main.cpp
)
target_link_libraries(${PROJECT_NAME} PUBLIC
Qt5::Core
Qt5::Widgets
)
main.cpp
#include <QApplication>
#include <QMainWindow>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMainWindow m ;
m.show();
return a.exec();
}
Запуск приложения под valgrind
Соберем наше приложение и запустим его под valgrind.
# bin/bash
export DIR="_debug_wid"
if ! [[ -d "$DIR" ]];
then
mkdir "$DIR"
fi;
cd $DIR
cmake -DCMAKE_BUILD_TYPE=Debug ../cmake-qt-widgets
make -j$(nproc)
valgrind --leak-check=full --error-limit=no --log-file=qt-widgets-raw.log ./cmake-qt-widgets
После завершения работы приложения, обратим свое внимание на полученные логи cmake-qt-widgets-raw.log, увидим примерно 89839 строк относительно однообразного содержания, приведем фрагмент лога:
==15974== 75,104 bytes in 2 blocks are still reachable in loss record 6,397 of 6,397
==15974== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==15974== by 0x846EB78: ??? (in /usr/lib/x86_64-linux-gnu/libfreetype.so.6.17.1)
==15974== by 0x84BA7B6: ??? (in /usr/lib/x86_64-linux-gnu/libfreetype.so.6.17.1)
==15974== by 0x8470073: FT_Load_Glyph (in /usr/lib/x86_64-linux-gnu/libfreetype.so.6.17.1)
==15974== by 0xAC020AE: ??? (in /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0)
==15974== by 0xAC02F0C: ??? (in /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0)
==15974== by 0xABAC53E: ??? (in /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0)
==15974== by 0xABAC77C: cairo_scaled_font_glyph_extents (in /usr/lib/x86_64-linux-gnu/libcairo.so.2.11600.0)
==15974== by 0xAB0F690: ??? (in /usr/lib/x86_64-linux-gnu/libpangocairo-1.0.so.0.4400.7)
==15974== by 0xAA6BCF4: pango_glyph_string_extents_range (in /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0.4400.7)
==15974== by 0xAA76EA4: ??? (in /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0.4400.7)
89825 ==15974== by 0xAA77208: ??? (in /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0.4400.7)
==15974==
==15974== LEAK SUMMARY:
==15974== definitely lost: 2,816 bytes in 5 blocks
==15974== indirectly lost: 15,015 bytes in 625 blocks
==15974== possibly lost: 2,330 bytes in 31 blocks
==15974== still reachable: 1,433,061 bytes in 18,173 blocks
==15974== of which reachable via heuristic:
==15974== length64 : 6,848 bytes in 77 blocks
==15974== newarray : 1,904 bytes in 39 blocks
==15974== suppressed: 0 bytes in 0 blocks
==15974==
==15974== Use --track-origins=yes to see where uninitialised values come from
==15974== For lists of detected and suppressed errors, rerun with: -s
==15974== ERROR SUMMARY: 41 errors from 41 contexts (suppressed: 0 from 0)
Мы запустили приложения уровня «Hello world», но получили огромное количество информации об ошибках в сторонних библиотеках. Мы конечно горим желанием разобраться со всеми этими ошибками, написать множество баг репортов, но ... потом. А в ближайших планах, временно, скрыть вывод информации об ошибках в сторонних библиотеках.
Генерация фильтров для valgrind
Запустим наше приложение под valgrind добавив флаг генерации исключений --gen-suppressions=all:
# bin/bash
export DIR="_debug_wid"
if ! [[ -d "$DIR" ]];
then
mkdir "$DIR"
fi;
cd $DIR
cmake -DCMAKE_BUILD_TYPE=Debug ../cmake-qt-widgets
make -j$(nproc)
valgrind --leak-check=full --error-limit=no --gen-suppressions=all --log-file=qt-widgets-gen.log ./cmake-qt-widgets
Лог будет дополнен некоторым количеством блоков вида:
{
<insert_a_suppression_name_here
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0.2400.32
fun:g_closure_invoke
obj:/usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0.6400.6
fun:g_signal_emit_valist
fun:g_signal_emit_by_name
fun:g_object_set_valist
fun:g_object_set
obj:/usr/lib/x86_64-linux-gnu/qt5/plugins/styles/libqgtk2style.so
obj:/usr/lib/x86_64-linux-gnu/qt5/plugins/styles/libqgtk2style.so
fun:_ZN13QStyleFactory6createERK7QString
fun:_ZN12QApplication5styleEv
fun:_ZN19QApplicationPrivate10initializeEv
}
Полученная информация поможет нам организовать сокрытие отчета об ошибках в используемых библиотеках. Подробнее о видах фильтрации логов можно прочитать в документации (https://valgrind.org/docs/manual/manual-core.html#manual-core.suppress). Нас же будет интересовать скрытие всей информации об ошибках в библиотеках. Создадим файл qwidget.supp и заблокируем вывод ошибок в лог :
Пример:
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libgtk-x11*
}
Все фильтры
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libgtk-x11*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libgtk-x11*
}
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libcairo*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libcairo*
}
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libX11*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libX11*
}
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libglib*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libglib*
}
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libfontconfig*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libfontconfig*
}
{
<insert_a_suppression_name_here>
Memcheck:Leak
...
obj:/usr/lib/x86_64-linux-gnu/libQt5*
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
...
obj:/usr/lib/x86_64-linux-gnu/libQt5*
}
Затем перезапустим с добавлением флага –suppressions, после которого укажем путь к файлу с фильтрами qwidget.supp
# bin/bash
export DIR="_debug_wid"
if ! [[ -d "$DIR" ]];
then
mkdir "$DIR"
fi;
cd $DIR
cmake -DCMAKE_BUILD_TYPE=Debug ../cmake-qt-widgets
make -j$(nproc)
valgrind --leak-check=full --error-limit=no --suppressions=../qwidget.supp --log-file=qt-widgets-test.log ./cmake-qt-widgets
Лог будет содержать только общую информацию о найденных ошибках
==74932== HEAP SUMMARY:
==74932== in use at exit: 1,694,522 bytes in 19,826 blocks
==74932== total heap usage: 71,338 allocs, 51,512 frees, 1,957,135,144 bytes allocated
==74932==
==74932== LEAK SUMMARY:
==74932== definitely lost: 0 bytes in 0 blocks
==74932== indirectly lost: 0 bytes in 0 blocks
==74932== possibly lost: 0 bytes in 0 blocks
==74932== still reachable: 0 bytes in 0 blocks
==74932== of which reachable via heuristic:
==74932== length64 : 6,848 bytes in 77 blocks
==74932== newarray : 1,904 bytes in 39 blocks
==74932== suppressed: 1,453,330 bytes in 18,834 blocks
==74932==
==74932== For lists of detected and suppressed errors, rerun with: -s
==74932== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 41 from 41)
Теперь удостоверимся в том, что новые ошибки не будут игнорироваться, изменим исходный код нашего приложения, добавив туда утечку памяти:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMainWindow m ;
m.show();
std::printf( "Run!\n");
int* leak = new int(5);
leak = nullptr;
return a.exec();
}
Сборка и запуск обновленного кода приведет к добавлению в лог информации о нашей ошибке:
4 bytes in 1 blocks are definitely lost in loss record 41 of 6,398
at 0x483BE63: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) by 0x10932D: main (main.cpp:16)
....
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 41 from 41)
Создание фильтров для санитайзеров
Удалим из нашего кода с утечку памяти:
int* leak = new int(5);
leak = nullptr;
Для запуска нашего приложения под санитайзерами придется предопределить переменные среды (CFLAGS, CXXFLAGS, LDFLAGS, LSAN_OPTIONS, ASAN_OPTIONS, UBSAN_OPTIONS):
# bin/bash
CUR_PATH=$PWD
PROJECT="qwgt"
echo $CUR_PATH
export DIR="_debug_$PROJECT"
if ! [[ -d "$DIR" ]];
then
mkdir "$DIR"
fi;
export DIR_SAN="_sanit_$PROJECT"
if ! [[ -d "$DIR_SAN" ]];
then
mkdir "$DIR_SAN"
fi;
cd $DIR
export CFLAGS="$CFLAGS -g -fsanitize=address,undefined"
export CXXFLAGS="$CXXFLAGS $CFLAGS"
export LDFLAGS="-fsanitize=address,undefined"
export LSAN_OPTIONS="fast_unwind_on_malloc=0"
export ASAN_OPTIONS="halt_on_error=0 detect_leaks=1"
export UBSAN_OPTIONS="print_stacktrace=1"
echo 'set env done.'
cmake -DCMAKE_BUILD_TYPE=Debug -DCHECK_COVER=ON -DUSE_SANITIZERS=ON ..
cmake --build . -- -j$(nproc)
cd ./cmake-qt-widgets
./cmake-qt-widgets 2> $CUR_PATH/$DIR_SAN/sanit.log
В логе мы увидим несколько тысяч строк примерно такого содержания:
Indirect leak of 3 byte(s) in 1 object(s) allocated from:
#27 0x7fbee755f39c in QApplicationPrivate::initialize() (/lib/x86_64-linux-gnu/libQt5Widgets.so.5+0x17139c)
#28 0x7fbee755f3f7 in QApplicationPrivate::init() (/lib/x86_64-linux-gnu/libQt5Widgets.so.5+0x1713f7)
#29 0x5561cf0908e3 in main /media/build/35CA936957ED7A1D/cppProj/cmake-code-cov-sanit/cmake-qt-widgets/main.cpp:12
SUMMARY: AddressSanitizer: 17831 byte(s) leaked in 630 allocation(s).
Т.е. санитайзеры, как и valgrind, детектируют ряд ошибок в системных библиотеках, но мы уже морально готовы к этому и, все еще, хотим получать информацию только об ошибках сделаных именно нами. Для этого создадим файл leak_suppr.txt и заполним его следующим текстом:
# This is a known leak.
leak:lsan_error::error
leak:QApplicationPrivate::init
leak:libgobject
leak:libpango
leak:libfontconfig
В документации санитайзеров (https://clang.llvm.org/docs/LeakSanitizer.html; https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer) о видах фильтрации логов инормации не так много, но примеры можно найти тут (https://sources.debian.org/src/qtwebengine-opensource-src/5.7.1+dfsg-6.1/src/3rdparty/chromium/build/sanitizers/lsan_suppressions.cc/ ; https://chromium.googlesource.com/external/github.com/google/proto-quic/+/0a5589a3da02d9e7eacf17dbef3ddaefc08b3f58/src/build/sanitizers/lsan_suppressions.cc). Добавим в переменную окружения LSAN_OPTIONS упоминание о нашем фильтре suppressions=$CUR_PATH/leak_suppr.txt и запустим наш код:
# bin/bash
CUR_PATH=$PWD
PROJECT="qwgt"
echo $CUR_PATH
export DIR="_debug_$PROJECT"
if ! [[ -d "$DIR" ]];
then
mkdir "$DIR"
fi;
export DIR_SAN="_sanit_$PROJECT"
if ! [[ -d "$DIR_SAN" ]];
then
mkdir "$DIR_SAN"
fi;
cd $DIR
export CFLAGS="$CFLAGS -g -fsanitize=address,undefined"
export CXXFLAGS="$CXXFLAGS $CFLAGS"
export LDFLAGS="-fsanitize=address,undefined"
export LSAN_OPTIONS="fast_unwind_on_malloc=0 suppressions=$CUR_PATH/leak_suppr.txt"
export ASAN_OPTIONS="halt_on_error=0 detect_leaks=1"
export UBSAN_OPTIONS="print_stacktrace=1"
echo 'set env done.'
cmake -DCMAKE_BUILD_TYPE=Debug -DCHECK_COVER=ON -DUSE_SANITIZERS=ON ..
cmake --build . -- -j$(nproc)
cd ./cmake-qt-widgets
./cmake-qt-widgets 2> $CUR_PATH/$DIR_SAN/sanit.log
Мы опять получим лог, который будет содержать только общую информацию о найденных ошибках:
-----------------------------------------------------
Suppressions used:
count bytes template
630 17831 libfontconfig
-----------------------------------------------------
Теперь вернем наш код с утечкой памяти:
int* leak = new int(5);
leak = nullptr;
Соберем и запустим наше приложение, используя приведенный выше код для фильтрации ошибок в сторонних библиотеках. В логе мы получим сведения только о нашей ошибке:
=================================================================
==18029==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x7f915636c587 in operator new(unsigned long) ../../../../src/libsani tizer/asan/asan_new_delete.cc:104
#1 0x5644af09eb22 in main /media/build/35CA936957ED7A1D/cppProj/cmake-co de-cov-sanit/cmake-qt-widgets/main.cpp:16
#2 0x7f91541dc082 in __libc_start_main ../csu/libc-start.c:308
#3 0x5644af09e73d in _start (/media/build/35CA936957ED7A1D/cppProj/cmake -code-cov-sanit/_debug_qwgt/cmake-qt-widgets/cmake-code-cov-s anit-qt-widgets+0x373d)
-----------------------------------------------------
Suppressions used:
count bytes template
630 17831 libfontconfig
-----------------------------------------------------
SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).
Заключение
Приведенные выше методы проверки кода на ошибки относительно просто объединить с запуском тестов и интегрировать в CI вашего проекта, что, без сомнения, повысит уровень качества вашего ПО и откроет новые грани восприятия, используемых вами, сторонних библиотек.