Я использовал tcmalloc в течение нескольких месяцев в большом проекте, и до сих пор я должен сказать, что я очень доволен этим, прежде всего его функциями HeapProfiling, которые позволяют отслеживать утечки памяти и устранять их.
За последние пару недель мы столкнулись со случайными сбоями в нашем приложении и не смогли найти источник случайного сбоя. В очень специфической ситуации, когда приложение рухнуло, мы оказались с полностью поврежденным стеком для одного из потоков приложения. Вместо этого я обнаружил, что потоки застряли в tcmalloc :: PageHeap :: AllocLarge (), но, поскольку у меня нет связанных символов отладки tcmalloc, я не мог понять, в чем проблема.
После почти недели исследования сегодня я попробовал самое простое: удалил tcmalloc из связи, чтобы избежать его использования, просто чтобы посмотреть, что произошло. Ну … я наконец-то выяснил, в чем проблема, и код, вызывающий проблемы, выглядит примерно так:
void AllocatingFunction()
{
Object object_on_stack;
ProcessObject(&object_on_stack);
}
void ProcessObject(Object* object)
{
...
// Do Whatever
...
delete object;
}
При использовании libc приложение все еще падало, но я наконец увидел, что вызываю delete для объекта, который был размещен в стеке.
Что я до сих пор не могу понять, так это почему tcmalloc поддерживает работу приложения независимо от этого очень рискованного (если не совершенно неправильного) освобождения объекта и двойного освобождения, когда object_on_stack выходит из области действия, когда завершается AllocatingFunction. Дело в том, что оскорбительный код можно было вызывать повторно без какого-либо намека на основную мерзость.
Я знаю, что освобождение памяти является одним из тех «неопределенных действий», когда они не используются должным образом, но мое удивление заключается в том, что между «стандартными» libc и tcmalloc такое поведение отличается.
У кого-нибудь есть какое-то объяснение понимания того, почему tcmalloc поддерживает работу приложения?
Заранее спасибо 🙂
Хорошего дня
очень рискованное (если не совершенно неправильное) освобождение объекта
хорошо, я не согласен здесь, это является совершенно неверно, и поскольку вы вызываете UB, может произойти все что угодно.
Это очень сильно зависит от того, что код tcmalloc делает с освобождением, и как он использует (возможно, мусор) данные в стеке в этом месте.
Я также видел сбой tcmalloc в таких случаях, а также glibc, входящий в бесконечный цикл. То, что вы видите, это просто совпадение.
Во-первых, не было двойного free
в твоем случае. Когда object_on_stack выходит из области видимости, нет free
вызов, просто указатель стека уменьшается (или, скорее, увеличивается, так как стек растет …).
Во-вторых, во время удаления TcMalloc должен уметь распознавать, что адрес из стека не принадлежит кучи программы. Вот часть free(ptr)
реализация:
const PageID p = reinterpret_cast<uintptr_t>(ptr) >> kPageShift;
Span* span = NULL;
size_t cl = Static::pageheap()->GetSizeClassIfCached(p);
if (cl == 0) {
span = Static::pageheap()->GetDescriptor(p);
if (!span) {
// span can be NULL because the pointer passed in is invalid
// (not something returned by malloc or friends), or because the
// pointer was allocated with some other allocator besides
// tcmalloc. The latter can happen if tcmalloc is linked in via
// a dynamic library, but is not listed last on the link line.
// In that case, libraries after it on the link line will
// allocate with libc malloc, but free with tcmalloc's free.
(*invalid_free_fn)(ptr); // Decide how to handle the bad free request
return;
}
Вызвать invalid_free_fn вылетает.