c++ 查找shared_ptr的引用计数递增的位置

qfe3c7zg  于 2023-01-10  发布在  其他
关注(0)|答案(3)|浏览(165)

我有一些代码在shared_ptr示例之间循环引用时发生内存泄漏(这是两个shared_ptr示例指向各自具有对另一个类示例的内部shared_ptr引用的对象的情况。这意味着没有一个类将被销毁,因为每个类示例仍由另一个使用,导致内存泄漏。在某些情况下,它也是引用自身的类的单个shared_ptr示例。)
通过Valgrind运行代码很有帮助,因为它告诉我内存最初分配的位置,但这不是循环引用的来源。我需要找到特定共享指针(Valgrind抱怨的那个)的引用计数增加的所有位置,因为其中一个必须更改为weak_ptr才能解决问题。
如何选择一个特定的shared_ptr,并获得其引用计数递增的所有源代码行的列表?
我在Linux下运行GCC/GDB和Valgrind,但是平台中立的解决方案会受到欢迎。
下面是演示该问题的一些示例代码:

#include <boost/shared_ptr.hpp>

struct Base {
    int i;
};
struct A: public Base {
    int a;
    boost::shared_ptr<Base> ptrInA;
};
struct B: public Base {
    int b;
    boost::shared_ptr<Base> ptrInB;
};

int main(void)
{
    boost::shared_ptr<A> a(new A);   // Line 17
    boost::shared_ptr<B> b(new B);
    a->ptrInA = b;                   // Line 19
    b->ptrInB = a;
    return 0;
}

在Valgrind下运行时,它会说:

HEAP SUMMARY:
    in use at exit: 96 bytes in 4 blocks
  total heap usage: 4 allocs, 0 frees, 96 bytes allocated

96 (24 direct, 72 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4
   at 0x4C2A4F0: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x40099A: main (test.cpp:17)

LEAK SUMMARY:
   definitely lost: 24 bytes in 1 blocks
   indirectly lost: 72 bytes in 3 blocks

我正在寻找一种解决方案,它可以将源文件中的第19-20行作为循环的可能原因,这样我就可以检查代码并决定是否需要更改它。

gmol1639

gmol16391#

基于@dandan78的方法,下面是GDB CLI的一个更详细的例子,它在shared_ptr的引用计数改变时创建一个断点。
main.cpp:

#include <iostream>
#include <memory>

using namespace std;

#define DBG(msg) std::cout << msg << std::endl;

class A {
    public:
        A(int i) {
            mI = i;
            DBG("A() this:"<<this<<" i:"<<mI);
        }
        ~A() {
            DBG("~A() this:"<<this<<" i:"<<mI);
        }
    private:
        int mI = 0;
};

int main() {
    std::shared_ptr<A> p1(new A(0x12345678));
    DBG("p1 use_count:"<<p1.use_count());
    {
        auto p2 = p1;
        DBG("p1 use_count:"<<p1.use_count());
        DBG("p2 use_count:"<<p2.use_count());
        auto p3 = p1;
        DBG("p1 use_count:"<<p1.use_count());
        DBG("p2 use_count:"<<p2.use_count());
        DBG("p3 use_count:"<<p3.use_count());
    }
    DBG("p1 use_count:"<<p1.use_count());
    return 0;
}

生成文件:

CXXFLAGS = -O0 -ggdb

main: main.cpp
    $(CXX) $(CXXFLAGS) -o $@ $<

程序输出:

A() this:0x6c6fb0 i:305419896
p1 use_count:1
p1 use_count:2
p2 use_count:2
p1 use_count:3
p2 use_count:3
p3 use_count:3
p1 use_count:1
~A() this:0x6c6fb0 i:305419896

编译并运行gdb(不要将#comments粘贴到gdb):

make
gdb main 2>&1 | tee out.log

广发会议:

(gdb) b main.cpp:23   # right after the p1 initialization
(gdb) r
Thread 1 hit Breakpoint 1, main () at main.cpp:23
(gdb) x/2xg &p1
0x62fe00:       0x0000000000fd4a10      0x0000000000fd4a50
# First pointer points to the target A object, sencond points to the reference counter
# Inspect the refcount data:
(gdb) x/4xw 0x0000000000fd4a50
0xfd4a50:       0x00405670      0x00000000      0x00000003      0x00000001
# The third integer is use_count of the shared_ptr, which can be printed by:
(gdb) x/1xw 0x0000000000fd4a50 + 8
0xfd4a58:       0x00000001

# Add a watchpoint for the use_count address
(gdb) watch *(int*)(0x0000000000fd4a50 + 8)
Hardware watchpoint 2: *(int*)(0x0000000000fd4a50 + 8)
# Add commands for the new watchpoint 2:
(gdb) commands 2
bt             # backtrace
c              # continue
end            # end of the handler script

(gdb) c        # Continue the program

现在,您可以检查out.log文件并分析use_count更改的所有回溯。
也可以直接添加gdb观察点:

watch *(*((int**)(&p1) + 1) + 2)
                   ^--------------- the shared_ptr variable
                         ^--------- +1 pointer to the right (+8 bytes in 64bit programm)
                              ^---- +2 integers to the right (+8 bytes)

如果使用优化编译,shared_ptr变量可能已经被优化掉了,只需直接在代码中打印出来,然后获取shared_ptr对象的地址并粘贴到gdb会话中即可:

std::cout << "p1:" << (void*)&p1 << std::endl;
cyej8jka

cyej8jka2#

虽然Yochai Timmer的方法对于较小的项目来说是不错的,但我最近不得不在一个相当大的代码库上工作,该代码库到处都使用了shared_ptr,Boost的变体。这是在Windows上进行的,UI是在MFC上完成的,CWinApp派生的主应用程序类由shared_ptr指向。而且它从未被析构。从而导致大量析构函数没有被调用,并导致一些令人讨厌的行为。
在尝试了各种泄漏检测器之后,我解决了这个问题,方法是让调试器在访问有问题的shared_ptr的第一行中断,然后搜索相关的头文件,直到找到引用计数器的确切位置。然后,我在引用计数器的地址上添加了一个内存断点,并让VS调试器在每次增量/递减,直到REF_CTR的值不能降回到其“正常”值。
在我的例子中,我知道这个shared_ptr在应用程序初始化完成后不应该有一个大于2的引用计数,当它达到3并且再也没有降回2时,我知道我找到了泄漏,并且内存断点只需要被达到大约1000次...
是的,我确信有更好的方法来跟踪shared_ptr的内存泄漏,但是如果所有方法都失败了,那么总是有监视引用计数器的暴力方法,当然,细节将取决于你的shared_ptr实现和你的应用程序是如何组织的。

ncecgwcz

ncecgwcz3#

您有一个设计错误。因此,您需要使用设计调试工具。
拿一支笔和一张纸。
为每一个类类型画一个矩形,从每一个拥有shared_ptr的类画一个箭头到它所拥有的类。
如果你找到一个圆,那是你的问题。
现在,每个箭头或链接都通过shared_ptr赋值在某处创建。
看看那些可疑的箭头,那些封闭圆圈的箭头,看看它们是否被正确地释放。

相关问题