Мотивация

Периодически встречающаяся проблемами кода на С и C++ являются утечки памяти и неопределенное поведение. Даже если вы используете умные указатели, то от ошибок в библиотеках сторонних разработчиков вы не застрахованы. Для поиска ошибок в коде существуют специальные инструменты:

  1. санитайзеры;

  2. 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 вашего проекта, что, без сомнения, повысит уровень качества вашего ПО и откроет новые грани восприятия, используемых вами, сторонних библиотек.

Комментарии (2)


  1. MiyuHogosha
    27.11.2023 08:22

    А что-то подобное для MSVS C++ возможно ? Та же беда, их Analyze находит проблемы в Qt и в protobuf, а так же в ряде их собственных библиотек.


    1. ost-vld Автор
      27.11.2023 08:22

      На сайте microsoft (https://learn.microsoft.com/ru-ru/cpp/sanitizers/asan?view=msvc-170) пишут: "AddressSanitizer, первоначально представленный Google ... ". Видимо, для фильтрации ошибок под MSVS, нужно переопределить константы сборки, добавив LSAN_OPTIONS="fast_unwind_on_malloc=0 suppressions=путь_до/leak_suppr.txt". Но это не точно...