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

重新发一贴,关于上一篇 C++ 代码中,结构体析构导致内存异常的问题,我做了更多的测试

  •  
  •   villivateur · 2023-05-19 12:22:57 +08:00 · 2610 次点击
    这是一个创建于 561 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上一篇是 https://v2ex.com/t/941007

    结构体定义:

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

    调用并崩溃的代码:

    void ESI_SetModuleIdentities(int slaveId, std::vector<uint32_t>& moduleIdentities)
    {
    	
    	ModuleConfig* newModule = new ModuleConfig;
    	printf("line: %d\n", __LINE__); // 917
    	newModule->identity = 243423;
    	printf("EEEEE %d\n", newModule->identity);
    	delete newModule;
    	printf("line: %d\n", __LINE__); // 921
    	newModule = NULL;
    	printf("line: %d\n", __LINE__); // 923
    
    	SlaveFileConfig* config = database[slaveId];
    	config->SetModuleIdentities(moduleIdentities);
    }
    

    直接运行后的打印输出:

    ModuleConfig::constructor
    line: 917
    EEEEE 243423
    ModuleConfig::destructor
    

    (只有这么多,打完这些就崩溃)

    使用 valgrind 调试,相关信息如下:

    ModuleConfig::constructor
    line: 917
    EEEEE 243423
    ModuleConfig::destructor
    ==8820== Conditional jump or move depends on uninitialised value(s)
    ==8820==    at 0x49D65CE: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28)
    ==8820==    by 0x170615: ModuleConfig::~ModuleConfig() (SlaveConfigParser.h:26)
    ==8820==    by 0x17F992: ESI_SetModuleIdentities(int, std::vector<unsigned int, std::allocator<unsigned int> >&) (SlaveConfigParser.cpp:920)
    ==8820==    by 0x161637: EcatAdapter::InitSlaveFileConfig() (EcatAdapter.cpp:1321)
    ==8820==    by 0x15F583: EcatAdapter::ConfigSlaves() (EcatAdapter.cpp:762)
    ==8820==    by 0x15DB78: EcatAdapter::Connect() (EcatAdapter.cpp:247)
    ==8820==    by 0x15C4D4: AdapterManager::Connect() (AdapterManager.cpp:250)
    ==8820==    by 0x15C0F6: AdapterManager::Run() (AdapterManager.cpp:151)
    ==8820==    by 0x15CA7F: StartAdapter (CExport.cpp:22)
    ==8820==    by 0x18F8CA: main (main.c:5)
    ==8820==  Uninitialised value was created by a heap allocation
    ==8820==    at 0x483BE63: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==8820==    by 0x17F939: ESI_SetModuleIdentities(int, std::vector<unsigned int, std::allocator<unsigned int> >&) (SlaveConfigParser.cpp:916)
    ==8820==    by 0x161637: EcatAdapter::InitSlaveFileConfig() (EcatAdapter.cpp:1321)
    ==8820==    by 0x15F583: EcatAdapter::ConfigSlaves() (EcatAdapter.cpp:762)
    ==8820==    by 0x15DB78: EcatAdapter::Connect() (EcatAdapter.cpp:247)
    ==8820==    by 0x15C4D4: AdapterManager::Connect() (AdapterManager.cpp:250)
    ==8820==    by 0x15C0F6: AdapterManager::Run() (AdapterManager.cpp:151)
    ==8820==    by 0x15CA7F: StartAdapter (CExport.cpp:22)
    ==8820==    by 0x18F8CA: main (main.c:5)
    ==8820== 
    ==8820== Conditional jump or move depends on uninitialised value(s)
    ==8820==    at 0x483CF75: operator delete(void*) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==8820==    by 0x170615: ModuleConfig::~ModuleConfig() (SlaveConfigParser.h:26)
    ==8820==    by 0x17F992: ESI_SetModuleIdentities(int, std::vector<unsigned int, std::allocator<unsigned int> >&) (SlaveConfigParser.cpp:920)
    ==8820==    by 0x161637: EcatAdapter::InitSlaveFileConfig() (EcatAdapter.cpp:1321)
    ==8820==    by 0x15F583: EcatAdapter::ConfigSlaves() (EcatAdapter.cpp:762)
    ==8820==    by 0x15DB78: EcatAdapter::Connect() (EcatAdapter.cpp:247)
    ==8820==    by 0x15C4D4: AdapterManager::Connect() (AdapterManager.cpp:250)
    ==8820==    by 0x15C0F6: AdapterManager::Run() (AdapterManager.cpp:151)
    ==8820==    by 0x15CA7F: StartAdapter (CExport.cpp:22)
    ==8820==    by 0x18F8CA: main (main.c:5)
    ==8820==  Uninitialised value was created by a heap allocation
    ==8820==    at 0x483BE63: operator new(unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==8820==    by 0x17F939: ESI_SetModuleIdentities(int, std::vector<unsigned int, std::allocator<unsigned int> >&) (SlaveConfigParser.cpp:916)
    ==8820==    by 0x161637: EcatAdapter::InitSlaveFileConfig() (EcatAdapter.cpp:1321)
    ==8820==    by 0x15F583: EcatAdapter::ConfigSlaves() (EcatAdapter.cpp:762)
    ==8820==    by 0x15DB78: EcatAdapter::Connect() (EcatAdapter.cpp:247)
    ==8820==    by 0x15C4D4: AdapterManager::Connect() (AdapterManager.cpp:250)
    ==8820==    by 0x15C0F6: AdapterManager::Run() (AdapterManager.cpp:151)
    ==8820==    by 0x15CA7F: StartAdapter (CExport.cpp:22)
    ==8820==    by 0x18F8CA: main (main.c:5)
    ==8820== 
    ==8820== Invalid free() / delete / delete[] / realloc()
    ==8820==    at 0x483CFBF: operator delete(void*) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
    ==8820==    by 0x170615: ModuleConfig::~ModuleConfig() (SlaveConfigParser.h:26)
    ==8820==    by 0x17F992: ESI_SetModuleIdentities(int, std::vector<unsigned int, std::allocator<unsigned int> >&) (SlaveConfigParser.cpp:920)
    ==8820==    by 0x161637: EcatAdapter::InitSlaveFileConfig() (EcatAdapter.cpp:1321)
    ==8820==    by 0x15F583: EcatAdapter::ConfigSlaves() (EcatAdapter.cpp:762)
    ==8820==    by 0x15DB78: EcatAdapter::Connect() (EcatAdapter.cpp:247)
    ==8820==    by 0x15C4D4: AdapterManager::Connect() (AdapterManager.cpp:250)
    ==8820==    by 0x15C0F6: AdapterManager::Run() (AdapterManager.cpp:151)
    ==8820==    by 0x15CA7F: StartAdapter (CExport.cpp:22)
    ==8820==    by 0x18F8CA: main (main.c:5)
    ==8820==  Address 0x4ff42e800000000 is not stack'd, malloc'd or (recently) free'd
    ==8820== 
    line: 921
    line: 923
    

    环境:g++ 9.4.0 / Ubuntu 20.04 / c++11

    std::string pdoMapName 改成 std::string pdoMapName{} 也不行

    第 1 条附言  ·  2023-05-19 13:47:37 +08:00

    修改结构体:

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

    打印:

    ModuleConfig::constructor
    0x55d602b78670 0x55d602b78678
    line: 916
    EEEEE 243423
    ModuleConfig::destructor
    0x55d602b78670 0x55d602b78674
    

    震惊!构造函数和析构函数里的变量地址不一样!

    第 2 条附言  ·  2023-05-19 14:31:43 +08:00
    查出来了,如上一帖 @lovelylain 所说的一样,在调用栈上层的上层的源文件包含的一个头文件里,存在 #pragma pack(push, 1) 但是忘了 pop 。

    之前居然没有出过问题……也可能之前偶发的 bug 跟这个有关。

    感谢所有人的热心
    28 条回复    2023-05-22 15:54:06 +08:00
    yolee599
        1
    yolee599  
       2023-05-19 12:58:46 +08:00 via Android
    你重新起一个只有这部分的代码,其他代码全删掉的工程看看呢?
    hefish
        2
    hefish  
       2023-05-19 13:00:13 +08:00
    看起来,结构体里面的 std::string 是个奇怪的东西。。。
    liuguangxuan
        3
    liuguangxuan  
       2023-05-19 13:05:28 +08:00
    可以按 1#说的,起一个单独的工程,把问题复现出来,发出来让大家调试一下。
    cnbatch
        4
    cnbatch  
       2023-05-19 13:09:34 +08:00
    旧版本的 lisbtdc++ 有 bug ,表现为 std::string 析构出错:
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82172

    这个 bug report 里面的版本是 libstdc++.so.6 ,而你给出的 error log 刚好也是链接到 libstdc++.so.6
    villivateur
        5
    villivateur  
    OP
       2023-05-19 13:37:25 +08:00
    @yolee599 只有这部的代码是正常的

    $ cat header.h
    #pragma once

    #include <stdio.h>
    #include <stdint.h>
    #include <string>

    struct ModuleConfig
    {
    ModuleConfig()
    {
    printf("ModuleConfig::constructor\n");
    }

    ~ModuleConfig()
    {
    printf("ModuleConfig::destructor\n");
    }

    uint32_t identity;
    std::string pdoMapName;
    uint32_t pdoMapInOffset;
    uint32_t pdoMapOutOffset;
    };

    $ cat main.cpp
    #include "header.h"

    int main()
    {
    ModuleConfig* newModule = new ModuleConfig;
    printf("line: %d\n", __LINE__);
    newModule->identity = 243423;
    printf("EEEEE %d\n", newModule->identity);
    delete newModule;
    printf("line: %d\n", __LINE__);
    newModule = NULL;
    printf("line: %d\n", __LINE__);

    return 0;
    }
    villivateur
        6
    villivateur  
    OP
       2023-05-19 13:43:24 +08:00
    @cnbatch Ubuntu20.04 已经确定修复了这个 bug
    roycestevie6761
        7
    roycestevie6761  
       2023-05-19 13:52:27 +08:00
    用 clang 或者其他编译器试一下不就行了
    leonshaw
        8
    leonshaw  
       2023-05-19 14:05:51 +08:00
    地址不一样只能怀疑编译器了,反汇编出来看看
    xiatwhu
        9
    xiatwhu  
       2023-05-19 14:13:53 +08:00
    是不是构造函数和析构函数不在一个源文件里面。比如构造函数在 a.cpp 里面,析构函数在 b.cpp 里面。编译 a.cpp 和 b.cpp 的时候传的编译选项不一样,导致 a 和 b 里面看到的 ModuleConfig 声明实际上不一样。重点检查一下 -fno-exceptions, -fno-rtti 编译选项。
    xiatwhu
        10
    xiatwhu  
       2023-05-19 14:20:19 +08:00
    也可能构造函数和析构函数都把实现放在了头文件里面。在 a.cpp 和 b.cpp 里面都包含了这个头文件,然后编译 a.cpp 和 b.cpp 的时候使用了不同的编译选项导致两个源码中看到的 ModuleConfig 对象声明实际上不一致。然后在 a.cpp 的里面 new 了 ModuleConfig 对象, 在 b.cpp 里面 delete ModuleConfig 对象。最后链接生成的可执行文件运行时就可能会报错。
    leonshaw
        11
    leonshaw  
       2023-05-19 14:22:17 +08:00
    @xiatwhu 有道理,看起来是两边对齐不一样。
    kkhaike
        12
    kkhaike  
       2023-05-19 14:28:20 +08:00
    你不给完整代码,没法帮你排查。首先你也测试了提出的代码是没问题的。

    c++的莫名问题,有时候都是和崩溃地方不相关的地方引起的,特别是 stackoverflow 这种
    villivateur
        13
    villivateur  
    OP
       2023-05-19 14:32:23 +08:00
    @xiatwhu
    @leonshaw
    @kkhaike

    感谢回复,已经排查到,见附言
    tomychen
        14
    tomychen  
       2023-05-19 14:38:26 +08:00
    ➜ testcpp g++ -o def main.cpp
    ➜ testcpp ./def
    ModuleConfig::constructor
    0x2587c20 0x2587c28
    line: 5
    EEEEE 243423
    ModuleConfig::destructor
    0x2587c20 0x2587c28
    line: 9
    line: 11
    g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
    Copyright (C) 2015 Free Software Foundation, Inc.



    /usr/local/bin/g++ --version
    g++ (GCC) 9.2.0
    Copyright (C) 2019 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions. There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

    ➜ testcpp /usr/local/bin/g++ main.cpp -o g9x
    ➜ testcpp ./g9x
    ModuleConfig::constructor
    0x19fbc20 0x19fbc28
    line: 5
    EEEEE 243423
    ModuleConfig::destructor
    0x19fbc20 0x19fbc28
    line: 9
    line: 11

    clang++ --version
    clang version 10.0.1 ( https://github.com/llvm/llvm-project.git ef32c611aa214dea855364efd7ba451ec5ec3f74)
    Target: x86_64-unknown-linux-gnu
    Thread model: posix
    InstalledDir: /usr/local/clang-10/bin
    ➜ testcpp ./ll
    ModuleConfig::constructor
    0x2262c20 0x2262c28
    line: 5
    EEEEE 243423
    ModuleConfig::destructor
    0x2262c20 0x2262c28
    line: 9
    line: 11


    感觉,你可以换台机器测测了...
    ohwind
        15
    ohwind  
       2023-05-19 14:50:47 +08:00
    > 存在 #pragma pack(push, 1) 但是忘了 pop 。

    我觉得这编译器应该给警告出来
    villivateur
        16
    villivateur  
    OP
       2023-05-19 14:52:14 +08:00
    @ohwind 然而并没有……
    saturn7
        17
    saturn7  
       2023-05-19 15:32:56 +08:00
    都使用到 c++, 还要魔改编译器结构体内存对齐,真没必要,浪费时间。
    villivateur
        18
    villivateur  
    OP
       2023-05-19 15:46:57 +08:00
    @saturn7 嵌入式设备,要做硬件通讯,没办法
    newmlp
        19
    newmlp  
       2023-05-19 16:07:05 +08:00
    @ohwind msvc 会有警告...巨硬牛逼!
    loveumozart
        20
    loveumozart  
       2023-05-19 16:16:25 +08:00
    写 c++太辛苦。。。排查这么一个 bug 都到编译器水平了,这鸟行业还 35 优化,程序员太苦逼了
    cnbatch
        21
    cnbatch  
       2023-05-19 17:01:28 +08:00 via Android
    其实可以换个没这种 bug 的编译器吧,嵌入式设备的环境又不是必须固定使用 Ubuntu 20.04
    Rothschild
        22
    Rothschild  
       2023-05-19 17:30:01 +08:00
    @saturn7 恰恰是 c++需要的开发地方,对齐内存是很常见的事儿,这不算魔改算日常操作
    exch4nge
        23
    exch4nge  
       2023-05-19 17:42:50 +08:00
    按理来说这种情况编译器应该有 warning 的,我也没环境无法确定,也可能是用参数忽略掉了这种 warning ,也可能是编译过程有大量 warning 输出大家都不看。如果这版本 gcc 真没有(可能性很小)那建议升级或换 clang 。
    junmoxiao
        24
    junmoxiao  
       2023-05-19 18:14:47 +08:00
    @loveumozart 有一说一不是 c++的锅,自己改了内存对齐能怪谁呢
    saturn7
        25
    saturn7  
       2023-05-19 18:31:51 +08:00
    @Rothschild 都用到 stl ,class ,object 了,修改内存对齐肯定不是日常操作呀!!!正常项目代码编程时,注意减少对象内存复制,基本可以减少硬件 90%内存压力。如果真是要求内存极限使用场景,正确是做法是根据自己公司业务,用 C/C++封装一套自己的数据容器才是明智的做法。我认为用到 c++,还去结构体还要修改内存对齐的方式,肯定算是铤而走险做法。
    Rothschild
        26
    Rothschild  
       2023-05-19 19:09:09 +08:00
    @saturn7 这只能说明你压根就没在过什么 C++的组里干过,内存对齐在网络传输序列化反序列化以及嵌入式行业是必须的,而且无处不在,你在任何大点 c++项目的代码里都能看到这些
    saturn7
        27
    saturn7  
       2023-05-22 09:51:11 +08:00
    @Rothschild 用过 google protobuf2/3 ,modbus, focas, 没见过那一个协议官方文档建议开发者手动改 pragma 对齐。不过既然是用网络流通信了,发送 /接收和内存对齐有毛关系,通用做法不都是大端字节序编码?
    Rothschild
        28
    Rothschild  
       2023-05-22 15:54:06 +08:00
    @saturn7 所以说你见得太少了啊,你为什么以为大型项目里只是那点 crud 的代码?你不会在 github 里搜一下#pragma pack(push,1)看看用的多频繁?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2789 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 12:47 · PVG 20:47 · LAX 04:47 · JFK 07:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.