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

c++如何判断二进制相同的对象?

  •  
  •   LuckyPocketWatch · 2023-08-17 00:23:58 +08:00 · 2990 次点击
    这是一个创建于 469 天前的主题,其中的信息可能已经有所发展或是发生改变。

    假如有两个自定义类 class A 和 class B ,这两个自定义类的对象长度均为 32 字节,然后有如下代码

    A* ptr = new A(/*paratemerA*/);  //先生成一个 class A 的对象为 objectA
    A* objectA = ptr;
    
    if(/*condition == true*/){
        ptr->~A();  //销毁对象 class A ,但内存没有还给系统
        ptr = new B(/*paratemerB*/);//在原来的内存上生成一个 class B 的对象为 objectB
    }
    
    if(ptr == objectA)
        ptr->doSomething();
    

    objectA 和 objectB 这两个类对象,长度均为 32 字节,这两个对象创建在相同的内存里(创建时间不一样),然后这段代码在运行时出现了一个巧合,生成的这两个对象二进制完全一样

    那这种情况下 C++时如何判断 ptr == objectA 这段代码的?

    36 条回复    2023-08-18 00:25:37 +08:00
    codehz
        1
    codehz  
       2023-08-17 00:41:17 +08:00 via iPhone
    需要考虑的问题是为什么你会有一个无效的指针需要比较?有这个无效的指针在,你问题是解决不完的…
    sunstar
        2
    sunstar  
       2023-08-17 00:50:11 +08:00 via iPhone
    ABA 问题?可以看看 brpc 的对象池
    polaa
        3
    polaa  
       2023-08-17 00:54:47 +08:00 via iPhone
    感觉你可能需要看 malloc 或者 calloc 相关堆实现
    包括 free chunk 之后插入 fast bin 或者 tcache 之类的
    好像涉及 uaf 或者 undefined behavior 漏洞吧
    堆相关知识忘光了 就这些吧
    Inn0Vat10n
        4
    Inn0Vat10n  
       2023-08-17 00:55:25 +08:00
    c++这里比的就是指针里存的地址,实际指向什么,指向的东西是不是合法的是不管的
    ashong
        5
    ashong  
       2023-08-17 01:18:57 +08:00 via iPhone
    调用了析构而没有删除,但新的 ptr 应该是指向新的 B 对象
    xiadong1994
        6
    xiadong1994  
       2023-08-17 01:52:32 +08:00
    你是可以要求编译器在指定的内存地址里面 new object 的,所以这不一定是巧合。https://isocpp.org/wiki/faq/dtors#placement-new

    ptr 能放 B 对象的指针说明这是一个父类指针指向子类的情况?那么比较 A*和 B*地址的时候应该会隐式转换成父类指针再比较。同类型指针指向同样地地址就是相同的。
    ryd994
        7
    ryd994  
       2023-08-17 01:57:45 +08:00 via Android
    你这个情况不正常。既然 A 的内存没有还给系统,那下面的 new 分配内存的时候就不会分配到 A 的内存。否则内存管理就有 bug 了。libc 这么多年,你随便就遇到内存管理 bug 的概率基本可以忽略。

    最简单的办法就是全局内存日志。在构造和析构函数里打日志用来分析

    一般没人乱飞野指针的。一般是配合引用计数或者 autoptr/smartptr 之类的使用。
    那引用计数也可以打日志
    ryd994
        8
    ryd994  
       2023-08-17 01:59:30 +08:00 via Android
    如果你只是想区分 A 对象和 B 对象,那让对象储存自己的名字即可,在构造函数里写入这个名字
    lany
        9
    lany  
       2023-08-17 03:10:41 +08:00
    objectA 指针里面存的 class A 的起始地址
    geelaw
        10
    geelaw  
       2023-08-17 03:12:09 +08:00
    假设你的代码是

    A *ptr = new A();
    A *objectA = ptr;
    ptr->~A();
    ptr = new (ptr) B(); // 注意这里需要用 placement new
    if (ptr == objectA) ptr->doSomething();

    并且 B 是 A 的派生类,并且假设 struct B : A { /* 没有额外的成员 */ };

    第一,我不知道 new (ptr) B() 是否是未定义行为,这需要翻阅标准。(适合于 A 的 storage 一定适合于 B 吗?)

    第二,接下来比较 ptr 和 objectA 绝对是 undefined behavior ,因为 objectA 最后一次赋值的时候指向的对象已经不存在了,所以 objectA 不是有效的指针——这件事情和原来的 storage 上面现在有没有 B 类型的对象、B 是不是 A 的子类没有任何关系。

    让我来写一个正确的版本:

    char alignas(B) storage[sizeof(B)];
    A *ptr = new (storage) A();
    A *objectA = ptr;

    ptr->~A();
    ptr = new (ptr) B();

    objectA = std::launder(objectA); // 这一步非常重要

    if (objectA == ptr) ptr->doSomething(); /* if 的 true 分支会运行 */
    geelaw
        11
    geelaw  
       2023-08-17 03:16:01 +08:00
    @geelaw #10 原来的问题:C++ 如何判断 ptr == objectA 这段代码的?

    既然是未定义行为,编译器可以选择格式化你的硬盘,但大多数编译器并不会这样做。
    在我转写的第一段代码里,比较可能发生的有两种:

    1. 无事发生,直接进行数值的比较,因此 true 分支会运行。
    2. 编译器意识到
    (i) ptr 赋值获得的是新对象
    (ii) objectA 在 ptr 赋值之后就没有再赋值过
    于是意识到 objectA 和 ptr 在标准看来不可能同时有效地指向同一个对象,因此直接删除 if 的 true 分支,导致 true 分支不执行。
    dangyuluo
        12
    dangyuluo  
       2023-08-17 04:06:09 +08:00
    ```
    ptr->~A(); //销毁对象 class A ,但内存没有还给系统
    ptr = new B(/*paratemerB*/);//在原来的内存上生成一个 class B 的对象为 objectB
    ```
    并不能保证在原来的内存上生成一个 class B ,除非你用了 placement new

    而且这是个 UB
    dangyuluo
        13
    dangyuluo  
       2023-08-17 04:11:28 +08:00
    @gleelaw `operator==`在两个同类型指针上应该是 well defined 。`*ptr`才是 UB 。

    https://godbolt.org/z/7Kzs4x4Mn
    iceheart
        14
    iceheart  
       2023-08-17 06:10:46 +08:00 via Android
    就是地址比较,地址肯定不同,所以 if 条件肯定不成立
    lopssh
        15
    lopssh  
       2023-08-17 07:48:27 +08:00 via Android
    你这代码会在编译时就被咔擦掉吧。。。
    你可以将 A*的变量赋值为 B*,你这 A/B 是相互不兼容的吧?那不是动态类型语言的活儿么。
    lopssh
        16
    lopssh  
       2023-08-17 07:50:02 +08:00 via Android
    @sunstar ABA 是先 A 然后 B ,然后再 A ,丢失了有关于 B 的信息,这个例子就只是 AA 。
    lopssh
        17
    lopssh  
       2023-08-17 07:53:38 +08:00 via Android
    A* ptr = new A(/*paratemerA*/); //先生成一个 class A 的对象为 objectA
    A* objectA = ptr;

    if(/*condition == true*/){
    ptr->~A(); //销毁对象 class A ,但内存没有还给系统
    }

    if(ptr == objectA)
    ptr->doSomething();

    我看你还是用这个来举例吧。
    那句 ptr 的赋值毫不影响程序逻辑,如果 B 是 A 的子类的话,
    blinue
        18
    blinue  
       2023-08-17 08:41:57 +08:00
    你对指针有很大误解,学好基础啊
    hankai17
        19
    hankai17  
       2023-08-17 08:43:03 +08:00
    感觉是设计不合理 出现内存池条件竞争
    可以看一下 brpc 内存池管理
    mingl0280
        20
    mingl0280  
       2023-08-17 09:02:56 +08:00 via Android
    你不会觉得 ptr = new B 能编译过吧?
    mingl0280
        21
    mingl0280  
       2023-08-17 09:05:01 +08:00 via Android
    @dangyuluo 他原来给的这个代码,除非 ptr 是 void*而且赋值时用了强转,否则怎么想都无法通过编译吧?
    hxysnail
        22
    hxysnail  
       2023-08-17 09:13:43 +08:00
    如果你内存没有还给系统,那么 B 不可能用 A 原来的内存;
    如果你内存已经还给系统了,objectA 还指向被回收内存,那就是野指针问题,严重 BUG……
    sloknyyz
        23
    sloknyyz  
       2023-08-17 09:36:34 +08:00
    建议再好好学学指针相关的内容,你问的是很基础的东西了。判断 ptr==objectA,很简单,指针判断是否一样就是判断指针值是否一样,根本不会管指针指向了什么内容。你的代码里,按照你说的,两个对象内存相同,那指针的值就是一样的,因此最后的判断是 true 。还有个问题,就是 ptr = new B(), 这段代码是编译不过的,但如果想可以加个强制转换。
    antonius
        24
    antonius  
       2023-08-17 09:51:13 +08:00
    Q:如何判断 ptr == objectA ?
    A:就是指针指向地址的比较。
    ptr = new B 不一定不能通过编译,如果 B 是 A 的子类,是可以的。
    不过 objectA 是一个野指针,不算是一个好的编码习惯。
    araraloren
        25
    araraloren  
       2023-08-17 10:10:05 +08:00
    我们 c++的程序总能搞出不一样的花样,反观 rust 就不太行
    jujusama
        26
    jujusama  
       2023-08-17 11:00:56 +08:00
    还得是你 cpp 啊,写出这种东西。

    正经回答:指针判等仅比较指针变量的值,参考最小示例

    https://godbolt.org/z/axPfGezab
    aneostart173
        27
    aneostart173  
       2023-08-17 11:14:09 +08:00
    你这是重写了 allocator?
    geelaw
        28
    geelaw  
       2023-08-17 11:19:59 +08:00
    @dangyuluo #13 贴代码无意义,不过您说得对,我需要修正 #10 #11 的说法,因为

    根据 [basic.compound]/3 ,指向对象的指针的值“表示的地址”是该对象(若该对象不在其生命周期内,则是该对象曾经、将要)占有的存储的第一个字节。这表示 placement new 返回和传入的指针虽然可能指向了不同的对象,但是它们“表示的地址”相同。

    根据 [expr.eq]/3.2 ,两个同类型、指向对象的、不是指向末尾之后的指针相等,如果它们“表示的地址”相等。

    #10 里面建议的 std::launder 不必要,并且 #11 里面认为的未定义行为不成立。

    ---

    回到 @dangyuluo #12 我认为应该尽量按照楼主希望的意思自动更正他的代码,所以应该认为

    struct A { }; struct B : A { };

    并且 new B() 改写为 new (ptr) B()。

    ---

    @sloknyyz #23 @antonius #24 @jujusama #26 指针比较并不是比较数值,考虑

    alignas(C) std::byte storage[2 * sizeof(C)];
    C *a = new (storage) C() + 1;
    C *b = new (storage + sizeof(C)) C();
    /* 此时 a 和 b 必然表示相同的地址 */
    a == b ? 1 : 2; /* 结果可以是 2 见 [basic.compound]/3 的 note 和 [expr.eq]/3.1 */

    再比如

    void *p = std::malloc(1);
    std::free(p);
    p == p ? 1 : 2; /* 结果可以是 2 见 [basic.stc]/4 */
    geelaw
        29
    geelaw  
       2023-08-17 11:32:36 +08:00
    @geelaw #28 贴代码无意义是指贴编译之后的机器代码无意义,因为未定义行为包括任何行为,不能根据实现推断标准。
    yeziqing
        30
    yeziqing  
       2023-08-17 11:37:14 +08:00   ❤️ 1
    @antonius 看他给的代码只调用了 A 的析构函数,所以这时所占用的内存没被释放,也即 ojbectA 指向的仍是有效内存地址,应该还不算是里指针。
    geelaw
        31
    geelaw  
       2023-08-17 12:29:43 +08:00
    @geelaw 最后一段里面第一个例子是错误的,因为数组是 implicit-lifetime object ,于是在 a == b 执行的时候它的效果已经变成了 [expr.eq]/3.2 了。不过第二个例子 p == p 依然成立。
    cnbatch
        32
    cnbatch  
       2023-08-17 13:57:53 +08:00
    既然 OP 没说实际业务情况,那我接下来的思路就怎么方便怎么来

    首先,既然两个 class 确定都是 32 字节,那么可以当成 int32_t

    A *ptr_a = &class_a_var;
    B *ptr_b = &class_b_var;
    cnbatch
        33
    cnbatch  
       2023-08-17 14:08:41 +08:00
    (没写完就发了出去,重来)

    既然 OP 没说实际业务情况,那我接下来的思路就怎么方便怎么来

    首先,既然两个 class 确定都是 32 字节,那么可以当成 int32_t

    A *ptr_a = &class_a_var; // 或者是 new 出来的
    B *ptr_b = &class_b_var; // 或者是 new 出来的

    int32_t value_a = *((int32_t *)ptr_a);
    int32_t value_b = *((int32_t *)ptr_b);

    std::map<int32_t, std::time_t> stores;

    然后把 32 位字节当成 int32_t 存起来:

    if (stores.find(value_a) == stores.end())
    stores[value_a] = std::time(nullptr);

    if (stores.find(value_b) == stores.end())
    stores[value_b] = std::time(nullptr);

    需要对比时就:

    if (stores.find(value_a) != stores.end())
    //时间已经存起来了,自己想怎么用就怎么用;

    if (stores.find(value_b) != stores.end())
    //时间已经存起来了,自己想怎么用就怎么用;
    antonius
        34
    antonius  
       2023-08-17 15:41:14 +08:00
    @yeziqing 感谢指正,你说的没错,不算野指针。
    tool2d
        35
    tool2d  
       2023-08-17 16:57:24 +08:00
    以前有个手动内存管理机制的 flag ,叫 Tomb 墓碑。能确保 ptr 销毁后,短期内地址不会被重复利用。
    kirory
        36
    kirory  
       2023-08-18 00:25:37 +08:00
    RTTI, typeid
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3225 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 12:58 · PVG 20:58 · LAX 04:58 · JFK 07:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.