Я хотел бы быть в состоянии определить, будет ли моя функция (или любая другая функция, которую она вызывает) в конечном итоге вызывать некоторые конкретные функции (например, 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
, но я бы предпочел не делать это для всех функций, которые меня интересуют.
Модульные тесты вызывают функции, которые они тестируют. Вы хотите знать, может ли функция F, вызванная модульным тестом, в конечном итоге вызвать malloc (или new, или …). Похоже, что вы действительно хотите сделать, это построить граф вызовов для всей вашей системы, а затем спросить критические функции F, может ли F достичь malloc и т. Д. В графе вызовов. Это довольно легко вычислить, если у вас есть граф вызовов.
Получить график звонков не так просто. Обнаружение того, что модуль A вызывает модуль B напрямую, «технически легко», если у вас есть реальный пользовательский интерфейс, который выполняет разрешение имен. Узнать, что А вызывает косвенно, нелегко; вам нужен (указатель на функцию) точки для анализа, и это сложно. И, конечно, вы должны решить, собираетесь ли вы погрузиться в библиотеку (например, процедуры 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 Software Reengineering Toolkit с его общим анализом потока и поддержкой генерации графа вызовов в сочетании с DMS C Front End, был использован для анализировать системы C из примерно 16 миллионов строк / 50000 функций с помощью косвенных вызовов создавать консервативно правильные графы вызовов.
Мы специально не пытались создавать графы вызовов C ++ для больших систем, но тот же общий анализ потоков DMS и генерация графов вызовов были бы «технически простыми» для DMS. C ++ Front End. При построении статического анализа, который работает правильно и в масштабе, нет ничего тривиального.
Если вы используете библиотеки, которые вызывают malloc, вы можете взглянуть на Joint Strike Fighter C ++ Стандарты кодирования. Это стиль кодирования, ориентированный на критически важное программное обеспечение. Одним из предложений будет написать свой собственный распределитель (и). Другое предложение состоит в том, чтобы использовать что-то вроде 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
ПРЕДУПРЕЖДЕНИЕ: Код не был затронут руками компилятора. Но ты получил идею.
Это не полный ответ, но вы можете попытаться использовать Valgrind для подсчета выделений и освобождений. Инструмент проверки по умолчанию Valgrind по умолчанию подсчитывает количество выделений, освобождает и печатает итоговый отчет в 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
}
Теперь вы можете запустить реальный тест и сравнить количество выделений с базовым тестом. Если они не равны, в вашем тестированном коде произошли некоторые выделения. Вы можете зафиксировать этот факт или сообщить о нем другим способом.
Если вы используете библиотеку GNU C, вы можете использовать _malloc_hook ()
и подобные функции, чтобы пользовательская функция вызывалась всякий раз, когда одна из функций malloc
семья используется.
Такая подключенная функция может анализировать трассировку вызова (используя backtrace()
) чтобы найти malloc
было разрешено в этой цепочке вызовов или нет и печатать сообщения о виновнике, если нет.