Обнаружение конкретных вызовов функций в модульных тестах

Я хотел бы иметь возможность определить, вызовет ли моя функция (или любая другая функция, которую она вызывает) некоторые определенные функции (например, malloc и free) в моих модульных тестах: некоторые небольшие части моего программного требования времени, и я хотел бы убедиться, что никто не добавляет что-то, что могло бы вызвать случайное выделение в этих функциях (и чтобы мой конвейер CI проверял это автоматически).

Я знаю, что могу просто поставить точку останова на gdb, но в идеале я хотел бы сделать что-то вроде:

void my_unit_test() {
    my_object obj; // perform some initialization that will allocate

    START_CHECKING_FUNCTION(malloc); // also, operator new or std::allocate would be nice
    obj.perform_realtime_stuff();
    STOP_CHECKING_FUNCTION(malloc);
}

в идеале тест провалится не слишком грязным образом (например, не std::abort), если в какой-то момент между двумя проверками будет вызвана функция malloc.

В идеале это должно работать на любой системе, но я могу жить с чем-то, что работает только на Linux. Возможно ли это каким-то образом? Может быть, через хак LD_PRELOAD, который заменит malloc, но я бы не хотел делать это для всех интересующих меня функций.


person Jean-Michaël Celerier    schedule 31.12.2017    source источник
comment
Некоторые реализации malloc отслеживают, сколько раз была вызвана функция, что помогает при отладке. Вы можете использовать это?   -  person 1201ProgramAlarm    schedule 31.12.2017
comment
Я просто не вижу прямой связи между требованиями жесткого реального времени и динамическим распределением памяти.   -  person seleciii44    schedule 31.12.2017
comment
@ seleciiii44 Требование состоит в том, чтобы не выполнять какое-либо динамическое выделение памяти.   -  person 1201ProgramAlarm    schedule 31.12.2017
comment
seleciii44: в основном, в обычных ядрах для настольных ПК, если вы вызываете malloc (или любые другие системные функции), есть большая вероятность, что ваш поток будет вытеснен ОС; даже если это не так, есть шанс, что он будет заблокирован, если другой поток одновременно вызовет malloc или free.   -  person Jean-Michaël Celerier    schedule 31.12.2017
comment
1201ProgramAlarm: немного посмотрел, и действительно кажется, что что-то вроде panthema.net/2013/malloc_count будет Работа.   -  person Jean-Michaël Celerier    schedule 31.12.2017
comment
Извините, это не похоже на жесткое реальное время, если вы тестируете модульные тесты независимо от того, вызываете ли вы malloc или нет. Вы не делаете жесткий реалтайм случайно.   -  person Antti Haapala    schedule 31.12.2017
comment
› Определение жесткого реального времени рассматривает любой пропущенный срок как системный сбой. Это действительно мой случай. Мы не сможем отправить продукт, если на эталонном оборудовании может быть пропущен хотя бы один крайний срок. Смертей не будет, но может быть потеряно довольно много денег.   -  person Jean-Michaël Celerier    schedule 31.12.2017
comment
Вместо того, чтобы выполнять модульные тесты, не было бы лучше (а) иметь политику отказа от динамического выделения памяти (за исключением запуска программы) и (б) проводить анализ всего исходного кода, чтобы убедиться, что динамическая память не используется. распределение как средство обеспечения соблюдения политики. Легче обойти такие вещи в модульных тестах, случайно или преднамеренно, поскольку модульные тесты полагаются на все соответствующие пути выполнения (проще сказать, чем достичь). Труднее обойти анализ всего источника, который обнаруживает использование функций или операторов.   -  person Peter    schedule 01.01.2018
comment
о (а), мне нужно иметь динамические распределения, НО я не хочу, чтобы они происходили в определенном потоке. например, поток A выделяет память в ответ на действие пользователя и отправляет указатель на вновь выделенную память потоку B (потоку реального времени) через свободную от блокировки очередь. Что касается (b), дело в том, что я использую стандартные структуры С++ (в основном векторные, flat_set/flat_map) и гарантирую, что у них достаточно памяти, прежде чем мне нужно будет войти в режим реального времени. Что я хочу, так это запретить кому-то другому, скажем, сделать некоторый vector::push_back в какой-то момент, который вызовет перераспределение.   -  person Jean-Michaël Celerier    schedule 01.01.2018
comment
И анализ всего исходного кода — это то, чем я сейчас занимаюсь, вручную и с постоянными проверками в heaptrack и valgrind, но вместо этого я хотел бы автоматизировать этот процесс, поскольку он требует относительно много времени.   -  person Jean-Michaël Celerier    schedule 01.01.2018
comment
Если ваше приложение работает в многопоточной системе с вытеснением (или даже просто с прерываниями), любое вытеснение может привести к сбою вашего расписания в реальном времени, а не только вызовы malloc. Так что вряд ли это полная проверка.   -  person Ira Baxter    schedule 01.01.2018
comment
Айра Бакстер: что еще могло стать причиной упреждения? (при условии, что поток использует планирование в реальном времени и имеет наивысший приоритет)   -  person Jean-Michaël Celerier    schedule 01.01.2018
comment
Как насчет того, чтобы просто заглянуть в файл карты компоновщика и проверить, есть ли там malloc? Вам абсолютно не нужно проверять это во время выполнения.   -  person tofro    schedule 13.01.2018
comment
tofro: проблема в том, что я не запрещаю malloc вообще, я запрещаю malloc в определенном дереве вызовов моей программы.   -  person Jean-Michaël Celerier    schedule 13.01.2018


Ответы (4)


Модульные тесты вызывают функции, которые они тестируют. Вы хотите знать, может ли функция F, вызываемая модульным тестом, в конечном итоге вызвать malloc (или new, или...). Похоже, что вы действительно хотите построить граф вызовов для всей вашей системы, а затем запросить критические функции F, может ли F достичь malloc и т. д. в графе вызовов. Это довольно легко вычислить, если у вас есть граф вызовов.

Получить график вызовов не так просто. Обнаружение того, что модуль A вызывает модуль B напрямую, «технически просто», если у вас есть реальный языковой интерфейс, который выполняет разрешение имен. Выяснить, что A вызывает косвенно, непросто; вам нужен (указатель функции) указывает на анализ, и это сложно. И, конечно же, вам решать, собираетесь ли вы погрузиться в библиотечные (например, std::) подпрограммы или нет.

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

Эта поддержка Doxygen утверждает, что строит графики звонков: http://clang.llvm.org/doxygen/CallGraph_8cpp.html Я не знаю, обрабатывает ли он косвенные вызовы/вызовы методов и насколько он точен; Я не очень хорошо знаком с этим, и документация кажется тонкой. В прошлом Doxygen не славился хорошей обработкой косвенности или точностью, но прошлые версии не были основаны на Clang. Существует некоторое дальнейшее обсуждение этого применения в небольших масштабах по адресу http://stackoverflow.com/questions/5373714/generate-calling-graph-for-c-code

Ваш вопрос помечен как c/c++, но, похоже, он касается C++. Для C — наш инструментарий для реинжиниринга программного обеспечения DMS с общим анализом потока и графиком вызовов. Поддержка генерации в сочетании с C Front End от DMS была используется для анализа систем C, содержащих около 16 миллионов строк/50 000 функций с косвенными вызовами для создания консервативно правильных графов вызовов.

Мы специально не пытались строить графы вызовов C++ для больших систем, но тот же общий анализ потока DMS и генерация графа вызовов были бы «технически простыми», используемыми с Внешний интерфейс C++. При построении статического анализа, который работает правильно и в масштабе, нет ничего тривиального.

person Ira Baxter    schedule 01.01.2018

Если вы используете библиотеки, которые вызывают malloc, вам может понадобиться взглянуть на Стандарты программирования C++ для Joint Strike Fighter. Это стиль кодирования, направленный на критически важное программное обеспечение. Одним из предложений было бы написать свой собственный распределитель (ы). Другое предложение состоит в том, чтобы использовать что-то вроде jemalloc, у которого есть статистика, но оно гораздо более непредсказуемо, поскольку оно ориентировано на производительность.


То, что вам нужно, это издевательская библиотека со шпионскими возможностями. То, как это работает для каждой платформы, будет отличаться, но вот пример с использованием Google:

static std::function<void*(size_t)> malloc_bridge;

struct malloc_mock {
    malloc_mock() { malloc_bridge = std::bind(&malloc_mock::mock_, this, _1); }
    MOCK_METHOD1(mock_, void*(size_t));
}

void* malloc_cheat(size_t size) {
    return malloc_bridge(size);
}

#define malloc malloc_cheat

struct fixture {
    void f() { malloc(...); }
};

struct CustomTest : ::testing::test {
    malloc_mock mock_;
};

TEST_F(CustomTest, ShouldMallocXBytes) {
    EXPECT_CALL(mock_, mock_(X))
      .WillOnce(::testing::Return(static_cast<void*>(0)));
    Fixture fix;
    fix.f();
}

#undef malloc

ВНИМАНИЕ: код не был затронут компилятором. Но ты получил идею.

person OwO    schedule 31.12.2017
comment
Проблема в том, что я не вызываю malloc напрямую. Но я использую структуры C++, такие как std::vector, boost::flat_map / flat_set и т. д., которые в какой-то момент могут вызвать libc malloc (иногда даже без malloc, появляющегося где-либо в исходном коде; например, new int[100] вызовет malloc за сцена хоть на линуксе но макросом не поймаешь) - person Jean-Michaël Celerier; 01.01.2018
comment
Вы можете использовать что-то вроде jemalloc, у которого, я думаю, есть статистика. - person OwO; 01.01.2018

Это не полный ответ, но вы можете попробовать использовать Valgrind для подсчета распределений и освобождений. Инструмент Valgrind по умолчанию memcheck по умолчанию подсчитывает количество распределений и освобождений и печатает результирующий отчет в HEAP SUMMARY, вот пример вывода:

$ valgrind ./a.out
==2653== Memcheck, a memory error detector
==2653== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2653== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2653== Command: ./a.out
==2653== 
==2653== 
==2653== HEAP SUMMARY:
==2653==     in use at exit: 0 bytes in 0 blocks
==2653==   total heap usage: 2 allocs, 2 frees, 72,716 bytes allocated
==2653== 
==2653== All heap blocks were freed -- no leaks are possible
==2653== 
==2653== For counts of detected and suppressed errors, rerun with: -v
==2653== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Вы можете добавить еще один тест, ничего не делающий для подсчета базового количества распределений:

void my_unit_test_baseline() {
    my_object obj; // perform some initialization that will allocate
}

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

person ks1322    schedule 05.01.2018

В случае, если вы используете библиотеку GNU C, вы можете использовать _malloc_hook () и подобные функции, чтобы пользовательская функция вызывалась всякий раз, когда используется одна из функций семейства malloc.

Такая перехваченная функция может анализировать трассировку вызовов (используя backtrace()), чтобы выяснить, разрешен ли malloc в этой цепочке вызовов или нет, и, если нет, вывести сообщения о виновнике.

person tofro    schedule 13.01.2018