V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
villivateur
V2EX  ›  C++

C++ 项目,出现了匪夷所思的 bug,在 vector 中添加对象,会导致 vector 崩溃,进而整个程序崩溃。

  •  
  •   villivateur · 2023-05-18 15:08:30 +08:00 · 6640 次点击
    这是一个创建于 586 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这个项目很大,我修改了其中一部分代码,出现了一个非常匪夷所思的问题:

    这个是出问题的函数:

    void SetModuleIdentities(std::vector<uint32_t>& identities)
    {
    	std::vector<ModuleConfig> testVec;
    	for (uint32_t const& identity : identities)
    	{
    		printf("enter... testVec.size=%ld %ld\n", testVec.size(), testVec.capacity());
    		ModuleConfig newModule;
    		newModule.identity = identity;
    		testVec.push_back(newModule);
    		printf("leave... testVec.size=%ld %ld\n", testVec.size(), testVec.capacity());
    	}
    }
    

    这是 ModuleConfig 的定义:

    struct ModuleConfig
    {
    	ModuleConfig()
    	{
    		printf("ModuleConfig::constructor\n");
    	}
    	~ModuleConfig()
    	{
    		printf("ModuleConfig::destructor\n");
    	}
        
    	uint32_t identity;
    	std::string pdoMapName;
    	uint32_t pdoMapInOffset;
    	uint32_t pdoMapOutOffset;
    };
    

    执行的输出如下:

    enter... testVec.size=0 0
    ModuleConfig::constructor
    leave... testVec.size=3353953467947191204 1
    ModuleConfig::destructor
    # 然后就崩溃了
    

    这是部分调用栈信息(来源 sighandler ):

    15:03:36  ecpanda exit with 11
    crash time:Thu May 18 15:03:36 2023
    
    ./base/lib-linux/bin/ecpanda-generic(_Z10sigHandleriP9siginfo_tPv+0x96) [0x56266aff3e9a]
    /lib/x86_64-linux-gnu/libpthread.so.0(+0x14420) [0x7f528d9d7420]
    /lib/x86_64-linux-gnu/libc.so.6(cfree+0x20) [0x7f528d50b6f0]
    ./base/lib-linux/bin/ecpanda-generic(_ZN12ModuleConfigD1Ev+0x2c) [0x56266b0075e6]
    ./base/lib-linux/bin/ecpanda-generic(_ZN15SlaveFileConfig19SetModuleIdentitiesERSt6vectorIjSaIjEE+0x136) [0x56266b017138]
    ./base/lib-linux/bin/ecpanda-generic(_Z23ESI_SetModuleIdentitiesiRSt6vectorIjSaIjEE+0x42) [0x56266b01692c]
    

    我就 push_back 了一下,怎么就把 vector 干崩了呢?

    第 1 条附言  ·  2023-05-18 16:59:11 +08:00
    目前做的测试:

    1. 把 ModuleConfig 里面的 std::string pdoMapName 删除,问题消失;
    2. 把 testVec 定义为 std::vector<ModuleConfig*> 然后通过 new 创建对象再 push_back 指针,问题消失
    第 2 条附言  ·  2023-05-18 17:08:05 +08:00
    进一步测试,这个崩溃与 vector 无关。只要调用 ModuleConfig 的析构函数就会崩溃
    第 3 条附言  ·  2023-05-19 14:31:02 +08:00
    查出来了,如 @lovelylain 所说的一样,在调用栈上层的上层的源文件包含的一个头文件里,存在 #pragma pack(push, 1) 但是忘了 pop 。

    之前居然没有出过问题……也可能之前偶发的 bug 跟这个有关。
    73 条回复    2023-05-31 22:00:12 +08:00
    nuk
        1
    nuk  
       2023-05-18 15:21:08 +08:00   ❤️ 1
    因为你的 newModule 是分配在栈上的吧
    villivateur
        2
    villivateur  
    OP
       2023-05-18 15:32:41 +08:00
    @nuk 分配在栈上,也没啥影响吧? push_back 会做一次 copy 的
    C47CH
        3
    C47CH  
       2023-05-18 15:33:50 +08:00
    用 compiler explorer 跑了下,没问题
    pagxir
        4
    pagxir  
       2023-05-18 15:34:48 +08:00 via Android
    这里的拷贝构造函数不能省吧。
    Rothschild
        5
    Rothschild  
       2023-05-18 15:41:39 +08:00
    compiler explorer 没有问题,所以是其他部分 bug
    villivateur
        6
    villivateur  
    OP
       2023-05-18 15:47:49 +08:00
    @C47CH
    @Rothschild

    如果是其他部分的 bug ,但我这里的 testVec 是局部变量。难道是其他地方把堆区破坏了吗?
    awinds
        7
    awinds  
       2023-05-18 15:51:10 +08:00
    代码看不出问题,可能是别的地方引起的
    nightwitch
        8
    nightwitch  
       2023-05-18 15:51:11 +08:00 via Android
    把参数 identities 的引用先去掉试试,这样传参的时候会复制一份。 如果你的是个多线程程序,在用 range based for 的时候其他线程对 vector 发生了增加或者删除,可能会 crash
    inhzus
        9
    inhzus  
       2023-05-18 15:52:27 +08:00   ❤️ 2
    感觉是其他地方把栈写坏了...
    jones2000
        10
    jones2000  
       2023-05-18 15:55:06 +08:00
    把 pdb 和 source 绑上去,报错的时候调用堆栈怎么都没有具体是对应哪一个文件里的哪一行代码, 调试的时候设置捕获所有异常,感觉是其他地方内存溢出或空指针,导致你这里报错了, 原始的错误的地方应该不在这里。
    diveIntoWork
        11
    diveIntoWork  
       2023-05-18 15:57:46 +08:00
    identity 这里,const 赋值给非 const ,应该有问题吧.....
    Rothschild
        12
    Rothschild  
       2023-05-18 16:04:12 +08:00
    这段代码定义了一个 ModuleConfig 结构,并使用 std::vector<ModuleConfig> 来存储多个 ModuleConfig 实例。函数 SetModuleIdentities 则根据传入的 identities 列表创建一组 ModuleConfig 并存储在 testVec 中。以下是我对这段代码的评审和建议。

    构造函数: 在 ModuleConfig 结构中,你定义了默认构造函数和析构函数,它们打印出一些信息。这在进行调试时可能有帮助,但在实际的项目中,我建议使用更正式的日志系统,而不是直接打印到控制台。

    成员初始化: ModuleConfig 结构的成员变量没有在构造函数中被初始化,这可能导致未定义行为。应当为每个成员变量提供默认值。

    预留容量: 当你知道要添加多少元素时,预先为 std::vector 保留足够的空间可以提高性能。你可以在创建 testVec 之后,使用 testVec.reserve(identities.size()) 来实现。

    使用 emplace_back 代替 push_back: 使用 emplace_back 可以在容器中直接构造对象,避免了临时对象的创建和拷贝。

    未使用的变量: testVec 在函数内部创建并填充,但在函数结束时被销毁,它的内容在函数外部无法使用。你可能需要将它作为函数的返回值或输出参数,使得其他函数或者代码块可以使用它的内容。
    ichao1214
        13
    ichao1214  
       2023-05-18 16:06:40 +08:00
    gdb 运行。然后崩溃的地方看下 info threads ,看下是不是多线程操作 vector 了
    blacktail
        14
    blacktail  
       2023-05-18 16:19:24 +08:00
    你第二行打印的 log ,size 是那么大个数,这已经是很有问题了。感觉并不是这里引起的崩溃,只是这里触发了而已。
    villivateur
        15
    villivateur  
    OP
       2023-05-18 16:23:56 +08:00
    @blacktail @jones2000 @inhzus @nightwitch 我也这么怀疑的,但是我用 gdb 看了,这个程序只有两个线程,另一个线程崩溃时处于 sleep 函数(而且业务逻辑上,这两个线程不会操作同一块内存区域)。

    有可能是在之前某个操作导致堆区已经坏了,然后这里触发的吗?

    @ichao1214 看了,并没有多线程操作
    sparklee
        16
    sparklee  
       2023-05-18 16:25:06 +08:00
    size 怎么这么大
    liuguangxuan
        17
    liuguangxuan  
       2023-05-18 16:34:40 +08:00 via Android
    如果怀疑是多线程的问题的话,链接上 asan 再跑一下试试。
    doraf
        18
    doraf  
       2023-05-18 16:35:48 +08:00
    @villivateur 去掉另外一个线程的话,还会崩溃吗?
    tkhmy
        19
    tkhmy  
       2023-05-18 16:39:17 +08:00
    代码没有问题,看 vector size 应该是多线程操作的锅了
    villivateur
        20
    villivateur  
    OP
       2023-05-18 16:42:53 +08:00
    @doraf 会崩溃的
    villivateur
        21
    villivateur  
    OP
       2023-05-18 16:43:06 +08:00
    @tkhmy 单线程也会崩溃
    ichao1214
        22
    ichao1214  
       2023-05-18 16:45:30 +08:00
    @villivateur 可能的,看看你修改的代码。回退是不是就好了哈哈
    mybyons
        23
    mybyons  
       2023-05-18 16:51:02 +08:00
    代码需要 refactor 的地方 chatGPT 已经说的很好了 参考 12 楼 @Rothschild

    你这里给出的信息不够 没有描述上下文的情况 简单来看最后的日志 size()[3353953467947191204] 和 cap()[1] 明显有问题

    你可以在 vector 模版那里 用自定义的 Allocator 在 allocate/deallocate 加一些打印消息 看看有没有思路
    codehz
        24
    codehz  
       2023-05-18 16:51:41 +08:00
    c++调试最麻烦的地方就在于,一些错误可以悄悄的传播到一个无关的代码上(几乎很难发现原始错误的地方)
    并且有些错误一旦挂了调试器 /santizer 就会消失(
    zizon
        25
    zizon  
       2023-05-18 16:52:28 +08:00
    看看生成的代码?
    异常堆栈里这个_ZN12ModuleConfigD1Ev 应该是析构函数里 free 某个东西的时候 segment fault 了.
    leonshaw
        26
    leonshaw  
       2023-05-18 16:59:29 +08:00
    用了什么魔改的库?
    villivateur
        27
    villivateur  
    OP
       2023-05-18 16:59:38 +08:00
    @mybyons

    目前做的测试:

    1. 把 ModuleConfig 里面的 std::string pdoMapName 删除,问题消失;
    2. 把 testVec 定义为 std::vector<ModuleConfig*> 然后通过 new 创建对象再 push_back 指针,问题消失
    villivateur
        28
    villivateur  
    OP
       2023-05-18 17:01:16 +08:00
    @leonshaw 没有魔改,都是标准库,g++ 9.4.0
    zizon
        29
    zizon  
       2023-05-18 17:01:55 +08:00
    string 的问题的话那可能是 ModuleConfig 的编译器生成的 copy constructor 有问题,double free 了?
    leonshaw
        30
    leonshaw  
       2023-05-18 17:05:01 +08:00
    @villivateur 有没有重载 operator new/delete
    wanglufeifei
        31
    wanglufeifei  
       2023-05-18 17:08:33 +08:00
    std::string 作为成员在结构体中是个地址,不是内存块,结构体字符串成员最好用 char[256]这种固定内存大小写法
    cnbatch
        32
    cnbatch  
       2023-05-18 17:27:24 +08:00
    如果不用 push_back ,而是用 emplace_back 呢?
    doraf
        33
    doraf  
       2023-05-18 17:29:22 +08:00
    用 Valgrind 这种试试,能有帮助没?
    cnbatch
        34
    cnbatch  
       2023-05-18 17:37:37 +08:00
    @cnbatch 忘了一个,emplace_back 的同时 std::move
    imagecap
        35
    imagecap  
       2023-05-18 17:38:44 +08:00
    看看是不是 printf 的问题,感觉这个地方 %ld 和 size_t 类型不匹配可能会破坏栈
    felixlong
        36
    felixlong  
       2023-05-18 17:41:51 +08:00
    用 compile explorer 里的 g++9.4.0 不能重现你的问题。大概率是运行到这个点的时候 heap 已经被弄坏了,可能哪段代码指针写出界了。
    loveumozart
        37
    loveumozart  
       2023-05-18 17:49:42 +08:00
    如果能复现就好解决了,不能复现就难解决了。
    能复现的话,就去把 core dump 文件拿出来,往下分析堆栈信息,具体崩溃原因是什么,这里说的崩溃原因至少要到 C++内存分配和释放源码级别,比如说某三个内存块之间在析构操作时被连接成同一个内存块的时候,如果中间那个内存块里保存的前后内存块的 size 和前后内存块 size 保存的自己的 size 对不上的话就会崩溃。(这是我之前遇到过的一个很难复现的 cpp 程序的一个例子)

    如果代码本身很简单,没什么问题,大概率就是内存被写坏了,用了不该用的指针,直接做了写操作,这在 C++很常见,和多线程没什么关系,写坏的时间点到崩溃的时间点中间有几分钟都很正常
    e7
        38
    e7  
       2023-05-18 17:51:34 +08:00   ❤️ 1
    从 size 看你 push 了很多很多很多下,栈溢出了,入参多大啊
    pkk007
        39
    pkk007  
       2023-05-18 17:52:18 +08:00
    或许可以尝试一下在类声明那里加一个`__declspec(align(64)) `
    loveumozart
        40
    loveumozart  
       2023-05-18 17:52:39 +08:00
    coredump 分析的时候,经常是要打印每个对象的每一个字节的内容是什么,最后才能验证结果,个人不是很喜欢 cpp 的这一点就是这个,第一次分析这种问题用了小一个周的时间才定位和验证出来
    corhuan
        41
    corhuan  
       2023-05-18 17:53:18 +08:00
    ModuleConfig 的定义贴全了吗?
    loveumozart
        42
    loveumozart  
       2023-05-18 17:54:00 +08:00
    还有就是在指针操作前后的地方多打日志,从日志角度运气好的话也可以看到在崩溃之前,就有一些写坏了的数据
    tkhmy
        43
    tkhmy  
       2023-05-18 17:55:16 +08:00
    @villivateur 试试在构造里把 pdoMapName 初始化一下
    anerevol
        44
    anerevol  
       2023-05-18 17:57:20 +08:00
    kkkbbb
        45
    kkkbbb  
       2023-05-18 18:58:55 +08:00
    原因找到了么?
    tomychen
        46
    tomychen  
       2023-05-18 19:13:07 +08:00
    @villivateur #2

    如果我没记错,push_back 是个 shallow copy ,也就是说...其实还是指针,这也解释了为什么后改为 new 了不会触发。而如果没有估计错,#4 说的,加个拷贝构造,应该也不会触发。
    786375312123
        47
    786375312123  
       2023-05-18 19:21:07 +08:00
    callstack 报错怎么说?
    786375312123
        48
    786375312123  
       2023-05-18 19:22:15 +08:00
    刚看到了,你这信息也太少了,在 vs 里跑跑试试。
    ashong
        49
    ashong  
       2023-05-18 19:28:46 +08:00
    leave... testVec.size=3353953467947191204 1

    应该是 push 出错, 导致返回 size 异常
    加个异常处理看看
    MrEatChicken
        50
    MrEatChicken  
       2023-05-18 19:49:34 +08:00
    其他地方的代码把栈写坏了。
    加 ASan 检测下哪里写坏了。
    https://zhuanlan.zhihu.com/p/360135083
    documentzhangx66
        51
    documentzhangx66  
       2023-05-18 20:21:52 +08:00
    不建议用 std::vector ,这玩意在设计阶段就有问题,该提供的功能没有,不需要的特性给你强塞一堆。

    建议自己实现一个纯粹的 queue 或 list 。

    要不去 github 找个 java 风格的。
    lovelylain
        52
    lovelylain  
       2023-05-18 20:59:06 +08:00 via Android   ❤️ 4
    检查下代码里有没有#pragma pack(n)对齐指令但没有#pragma pack()恢复默认,多年前遇到过这样一个案例,同事在他的头文件里 pack 对齐但没取消,我的结构体定义在我的头文件里,在不同 cpp 文件,先包含他的头文件再包含我的,和先包含我的或者不包含他的,我那个结构体的大小会不一样,于是也出现了匪夷所思的 crash ,害我 gdb 怼了小半天。
    villivateur
        53
    villivateur  
    OP
       2023-05-18 21:42:12 +08:00
    @imagecap printf 是因为出问题才加了调试的
    GeruzoniAnsasu
        54
    GeruzoniAnsasu  
       2023-05-18 21:48:59 +08:00   ❤️ 1
    @villivateur

    > 如果是其他部分的 bug ,但我这里的 testVec 是局部变量。难道是其他地方把堆区破坏了吗?

    vector 实际用到的内存一律在堆上,跟对象本身分配在栈上还是堆上无关。
    这种崩溃 bug 基本上都是 heap corruption ,建议还是开一下 santinizer 或者 valgrind 看看有没有帮助。



    heap corruption 是最难调的…… 因为导致污染的地方和触发 crash 的地方可能相差十万八千里…… 祝好运
    villivateur
        55
    villivateur  
    OP
       2023-05-18 22:14:14 +08:00
    @e7 只 push 了一次,那个 size 很大说明已经出问题了。

    话说已经确认跟 vector 没关系了,问题在 ModuleConfig 的析构函数。
    leonshaw
        56
    leonshaw  
       2023-05-18 22:19:04 +08:00
    @villivateur 还没解决?直接 gdb watch vector 实现里的 _M_impl._M_start 和 _M_impl._M_finish ,看什么时候写坏的
    villivateur
        57
    villivateur  
    OP
       2023-05-18 22:20:10 +08:00
    @leonshaw 哈哈,早下班了,明天再看。话说看下附言,已经确定跟 vector 没关系了,是那个 ModuleConfig 的析构函数的问题
    felixlong
        58
    felixlong  
       2023-05-18 22:21:47 +08:00
    @villivateur 看看你 ModuleConfig 定义的头文件里是不是有什么宏。导致 ModuleConfig 实现文件里的析构函数看到的 ModuleConfig 和 SetModuleIdentities 这里看到的 size 其实不一样。
    leonshaw
        59
    leonshaw  
       2023-05-18 22:22:44 +08:00   ❤️ 1
    @villivateur 但是错误的 size 是析构之前打出来的,析构只是踩到野地址挂了,之前应该就有问题。
    mingl0280
        60
    mingl0280  
       2023-05-18 22:29:54 +08:00 via Android
    高度怀疑你的 std::string 不是 std::string 而是其它的什么东西。
    Hconk
        61
    Hconk  
       2023-05-18 22:39:09 +08:00 via iPhone
    遇到过因为前面代码的 memcpy 一个不同大小的结构体把栈写坏了,导致在其他地方 crash ,一般这种奇怪问题都不是在挂的那行的问题,而是其他地方把内存写坏导致,lz 查清了可以回来更新下后续
    MetroWind
        62
    MetroWind  
       2023-05-19 03:08:28 +08:00
    是嵌入式么?如果是嵌入式的话有可能和系统处理堆的方式有关。
    不是的话就是其他地方有 undefined behavior⋯⋯
    billccn
        63
    billccn  
       2023-05-19 04:07:05 +08:00
    首先楼主应该说一下用的什么编译器和开的哪个语言标准。

    我看主要问题应该是那个`std::string pdoMapName`是一个非 POD 类型,在 C++11 之前非 POD 类型不会被默认初始化,楼主的 ModuleConfig()构造函数也没有初始化它,那这个字串里面存的指针就是之前栈里面一个垃圾数值。析构函数会把这个垃圾当成一个真的指针 free ,那肯定要崩溃的。

    楼主说“通过 new 创建对象再 push_back 指针,问题消失”那是应为每个新分配的指针指向了清 0 的页面,如果指针原来是有数据的,那也要崩溃。楼主可以自己用 placemnt new 试一下。

    写成`std::string pdoMapName{};`试试?
    villivateur
        64
    villivateur  
    OP
       2023-05-19 08:46:42 +08:00
    @billccn g++ 9.4.0 ,Ubuntu 20.04 ,C++ 11
    Plime
        65
    Plime  
       2023-05-19 09:05:21 +08:00 via iPhone
    @billccn 应该是正解
    tusj
        66
    tusj  
       2023-05-19 09:10:12 +08:00
    是不是有别的线程在改写 identities
    shyrock
        67
    shyrock  
       2023-05-19 09:16:50 +08:00
    @codehz #24 所以有时候我觉得调试 C++内存错误真的是在浪费生命。。。
    seanwhy
        68
    seanwhy  
       2023-05-19 09:49:27 +08:00
    作为一个过来人劝告结构体莫用 std::string ,应该是这里的问题,用 char 吧
    dnks
        69
    dnks  
       2023-05-19 09:51:29 +08:00
    这段代码可能会导致崩溃的原因是在 SetModuleIdentities 函数中,使用了一个 std::vector 容器来存储 ModuleConfig 结构体对象。在每次循环迭代中,都会向 testVec 容器中添加一个新的 ModuleConfig 对象。

    然而,ModuleConfig 结构体中包含一个 std::string 类型的成员变量 pdoMapName ,而在默认构造函数中,并没有对 pdoMapName 进行初始化。这会导致在每次循环迭代时,都会创建一个新的 ModuleConfig 对象,并使用默认构造函数进行初始化,但由于 pdoMapName 没有被正确初始化,可能会导致悬空指针或访问未定义的内存区域,从而引发崩溃。

    为了解决这个问题,你可以在 ModuleConfig 结构体的默认构造函数中对 pdoMapName 进行初始化。
    ---By ChatGPT
    coreki
        70
    coreki  
       2023-05-19 12:29:43 +08:00
    试试预先分配空间
    yh5DC6Y7u7TdcT9s
        71
    yh5DC6Y7u7TdcT9s  
       2023-05-19 14:30:03 +08:00
    1 内存对齐问题,检查哪里是不是设置了相关的参数
    2 std::string 符号被覆盖, 最好的办法, 在这个 for 循环里,调用 std::string 的一个函数,然后 gdb step into 里面去, 确定是不是 libstdc++里面的函数
    3 其他线程踩内存了, 直接开 sanitizer 跑下
    v2Mark
        72
    v2Mark  
       2023-05-31 19:59:16 +08:00
    @billccn 感觉这个老哥 说的很合理,
    1. 非 POD 类型,string 没有初始化,结合 op 说的 string 成员取消了就不会有问题。建议给 string 一个初始值再试试。
    2. 这个 size 是在是太大了,崩溃在 ModuleConfig 析构,那么 pushback 的时候,vector 的缓冲区应该分配了一段无效内存。 主要还是在 vector 扩容的时候,一般都是 2N 的,存在大量的 ModuleConfig 复制,感觉在这个时间段内存分配失败或者被其他地方占用了,也会崩溃。这个还需进一步验证;

    建议:
    1. 如果 string 必须是成员的话,可以每次 push 的时候看下 vector 的 capacity 和 size ,如果 capacity 不够,先进行 reverse 吧。
    2. C++11 可以用 emplace_back ,直接构建吧,避免了复制移动这些操作。(感觉这个 OK)
    villivateur
        73
    villivateur  
    OP
       2023-05-31 22:00:12 +08:00
    @v2Mark 看下附言,已经找到问题了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1042 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 19:41 · PVG 03:41 · LAX 11:41 · JFK 14:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.