当前位置: 首页 > 知识库问答 >
问题:

用于比较原始类型的std::可选的有趣程序集

涂溪叠
2023-03-14

在我的一个单元测试中,Valgrind获得了一个flurry条件跳转或移动依赖于未初始化的值。

检查程序集时,我意识到下面的代码:

bool operator==(MyType const& left, MyType const& right) {
    // ... some code ...
    if (left.getA() != right.getA()) { return false; }
    // ... some code ...
    return true;
}

所在的位置::getA() 常量 -

   0x00000000004d9588 <+108>:   xor    eax,eax
   0x00000000004d958a <+110>:   cmp    BYTE PTR [r14+0x1d],0x0
   0x00000000004d958f <+115>:   je     0x4d9597 <... function... +123>
x  0x00000000004d9591 <+117>:   mov    r15b,BYTE PTR [r14+0x1c]
x  0x00000000004d9595 <+121>:   mov    al,0x1

   0x00000000004d9597 <+123>:   xor    edx,edx
   0x00000000004d9599 <+125>:   cmp    BYTE PTR [r13+0x1d],0x0
   0x00000000004d959e <+130>:   je     0x4d95ae <... function... +146>
x  0x00000000004d95a0 <+132>:   mov    dil,BYTE PTR [r13+0x1c]
x  0x00000000004d95a4 <+136>:   mov    dl,0x1
x  0x00000000004d95a6 <+138>:   mov    BYTE PTR [rsp+0x97],dil

   0x00000000004d95ae <+146>:   cmp    al,dl
   0x00000000004d95b0 <+148>:   jne    0x4da547 <... function... +4139>

   0x00000000004d95b6 <+154>:   cmp    r15b,BYTE PTR [rsp+0x97]
   0x00000000004d95be <+162>:   je     0x4d95c8 <... function... +172>

    => Jump on uninitialized

   0x00000000004d95c0 <+164>:   test   al,al
   0x00000000004d95c2 <+166>:   jne    0x4da547 <... function... +4139>

我用x标记了在未设置可选项的情况下未执行(跳过)的语句。

成员< code>A在< code>MyType中的偏移量为< code>0x1c。检查< code>std::optional的布局,我们看到:

    < li> 0x1d对应于< code>bool _M_engaged, < li> 0x1c对应于< code > STD::uint 8 _ t _ M _ payload (在匿名联合内部)。

标准::的相关代码可选为:

constexpr explicit operator bool() const noexcept
{ return this->_M_is_engaged(); }

// Comparisons between optional values.
template<typename _Tp, typename _Up>
constexpr auto operator==(const optional<_Tp>& __lhs, const optional<_Up>& __rhs) -> __optional_relop_t<decltype(declval<_Tp>() == declval<_Up>())>
{
    return static_cast<bool>(__lhs) == static_cast<bool>(__rhs)
         && (!__lhs || *__lhs == *__rhs);
}

在这里,我们可以看到gcc对代码进行了相当大的转换;如果我理解正确的话,在C语言中这给出了:

char rsp[0x148]; // simulate the stack

/* comparisons of prior data members */

/*
0x00000000004d9588 <+108>:   xor    eax,eax
0x00000000004d958a <+110>:   cmp    BYTE PTR [r14+0x1d],0x0
0x00000000004d958f <+115>:   je     0x4d9597 <... function... +123>
0x00000000004d9591 <+117>:   mov    r15b,BYTE PTR [r14+0x1c]
0x00000000004d9595 <+121>:   mov    al,0x1
*/

int eax = 0;
if (__lhs._M_engaged == 0) { goto b123; }
bool r15b = __lhs._M_payload;
eax = 1;

b123:
/*
0x00000000004d9597 <+123>:   xor    edx,edx
0x00000000004d9599 <+125>:   cmp    BYTE PTR [r13+0x1d],0x0
0x00000000004d959e <+130>:   je     0x4d95ae <... function... +146>
0x00000000004d95a0 <+132>:   mov    dil,BYTE PTR [r13+0x1c]
0x00000000004d95a4 <+136>:   mov    dl,0x1
0x00000000004d95a6 <+138>:   mov    BYTE PTR [rsp+0x97],dil
*/

int edx = 0;
if (__rhs._M_engaged == 0) { goto b146; }
rdi = __rhs._M_payload;
edx = 1;
rsp[0x97] = rdi;

b146:
/*
0x00000000004d95ae <+146>:   cmp    al,dl
0x00000000004d95b0 <+148>:   jne    0x4da547 <... function... +4139>
*/

if (eax != edx) { goto end; } // return false

/*
0x00000000004d95b6 <+154>:   cmp    r15b,BYTE PTR [rsp+0x97]
0x00000000004d95be <+162>:   je     0x4d95c8 <... function... +172>
*/

//  Flagged by valgrind
if (r15b == rsp[097]) { goto b172; } // next data member

/*
0x00000000004d95c0 <+164>:   test   al,al
0x00000000004d95c2 <+166>:   jne    0x4da547 <... function... +4139>
*/

if (eax == 1) { goto end; } // return false

b172:

/* comparison of following data members */

end:
    return false;

这相当于:

//  Note how the operands of || are inversed.
return static_cast<bool>(__lhs) == static_cast<bool>(__rhs)
         && (*__lhs == *__rhs || !__lhs);

我认为组装是正确的,如果奇怪的话。也就是说,据我所知,未初始化值之间的比较结果实际上并不影响函数的结果(与C或C不同,我确实希望比较x86汇编中的垃圾不是UB):

    < li >如果一个可选的是< code>nullopt并且另一个被设置,则< code> 148处的条件跳转跳转到< code > end (< code > return false ),OK。 < li >如果两个选项都设置了,则比较读取初始化的值,OK。

因此,唯一感兴趣的情况是当两个选项都是< code>nullopt时:

    < li >如果值比较相等,则代码断定选项相等,这是真的,因为它们都是< code>nullopt, < li >否则,如果< code>__lhs,则代码断定选项相等。_M_engaged为假,为真。

因此,无论哪种情况,代码都得出结论,当两个选项都为nullopt时,这两个选项是相等的;CQFD公司。

这是我第一次看到gcc生成明显“良性”的未初始化读取,因此我有几个问题:

  1. 未初始化的读取在程序集中是否正常(x84_64)?
  2. 这是否是在非良性情况下可能触发的优化失败(||逆转)的综合征?

目前,我倾向于使用优化(1)注释少数函数,作为防止优化生效的解决方案。幸运的是,已识别的函数并不是性能关键。

环境:

  • 编译器:gcc 7.3
  • 编译标志:-std=c 17-g-Wall-Werror-O3-flto(适当的包含)
  • 链接标志:-O3-flto(适当的库)

注:可以用-O2代替-O3出现,但决不能没有

有趣的事实

在完整代码中,此模式在上面概述的函数中出现 32 次,用于各种有效负载:std::uint8_tstd::uint32_tstd::uint64_t,甚至是结构 { std::int64_t; std::int8_t; }

它只出现在几个大运算符==中,比较具有~40个数据成员的类型,而不是较小的类型。并且它不会出现在标准::可选

最后,令人愤怒的是,将所讨论的函数隔离在它自己的二进制中会使“问题”消失。事实证明,神话般的MCVE是难以捉摸的。


共有3个答案

徐晔
2023-03-14

我不太确定它是由编译器错误引起的。您的代码中可能有一些UB,它允许编译器更积极地优化您的代码。无论如何,对于问题:

  1. UB不是程序集中的问题。在大多数情况下,您所指的地址下面的内容将被读取。当然,大多数操作系统在将内存页提供给程序之前都会填满内存页,但您的变量很可能驻留在堆栈中,所以很可能它包含垃圾数据。Soo,只要你能接受随机数据比较(这很糟糕,因为可能会错误地给出不同的结果),那么汇编是有效的
  2. 很可能是反向比较综合症
宋琛
2023-03-14

在x86 asm中,最糟糕的情况是单个寄存器有一个未知值(或者在可能的内存排序的情况下,您不知道它有两个可能的值,旧的还是新的)。但是如果你的代码不依赖于那个寄存器值,你就很好,不像在C中,C UB意味着你的整个程序在一个有符号整数溢出之后,理论上是完全无用的,甚至在此之前,沿着编译器可以看到的代码路径将会导致UB。在asm中从来没有发生过这样的事情,至少在非特权用户空间代码中没有。

(你可能会做一些事情,通过以奇怪的方式设置控制寄存器或将不一致的东西放入页表或描述符中,从而在内核中导致系统范围内的不可预测行为,但这种情况不会发生,即使你在编译内核代码。)

有些isa具有“不可预测的行为”,如早期ARM。如果对乘法的多个操作数使用同一个寄存器,行为是不可预测的。IDK如果这允许打破管道和破坏其他寄存器,或者如果它仅限于一个意外的乘法结果。我猜是后者。

或者MIPS,如果将分支放在分支延迟槽中,则行为是不可预测的。(由于分支延迟槽,处理异常很混乱...但大概仍然有限制,你不能使机器崩溃或破坏其他进程(在像Unix这样的多用户系统中,如果一个非特权用户空间进程可能会破坏其他用户的任何东西,那将是很糟糕的)。

非常早期的MIPS也有加载延迟槽和乘法延迟槽:你不能在下一条指令中使用加载的结果。如果你过早读取寄存器,你可能会得到寄存器的旧值,或者可能只是垃圾。MIPS=最小互锁管道阶段;他们想把停滞转移给软件,但结果是,当编译器找不到任何有用的东西来处理下一个臃肿的二进制文件时,添加NOP会导致整体代码变慢,而不是在必要时让硬件停滞。但是我们被分支延迟槽困住了,因为删除它们会改变ISA,不像放松对早期软件没有做的事情的限制。

司马作人
2023-03-14

x86整数格式中没有陷阱值,因此读取和比较未初始化的值会生成不可预知的真/假值,不会产生其他直接危害。

在加密上下文中,导致采用不同分支的未初始化值的状态可能会导致计时信息泄漏或其他旁路攻击。但是加密加固可能不是您所担心的。

gcc在读取是否给出错误值无关紧要的情况下进行未初始化读取这一事实并不意味着它会在重要的时候进行。

 类似资料:
  • 我对在Java中实现一个特殊的优先级队列变体很感兴趣,我希望这个优先级队列能够与泛型类型一起工作。在Java的集合对象中,存储具有某种排序的对象(例如PriorityQueue、TreeSet等),可以使用实现Compariable的类以及不一定实现Compariable的类,因为类的比较器传递给构造函数。 如何在优先级队列类中实现此功能?如果给我一个比较器,我是否必须根据类是否实现Compara

  • 问题内容: 我得到的错误就在这行 。 该怎么办?其他逻辑还可以吗? 我想做的是有一个A列表和一个B列表,其中一个属性与id相同;尽管变量名不同。即在和在B。现在我将两个列表都放在ListAll中,并在相同的变量id / bid上对它们进行排序。我有A和B实现可比性。 和我的listAll是对象类型? 我该怎么做?谢谢。 问题答案: 您可以添加一个通用基类并在那里进行比较,如下所示:

  • 问题内容: 我需要编写一个比较器,它采用类型A的对象A和类型B的对象B。这两个对象不是公共对象的扩展。它们的确不同,但是我需要通过其中的通用字段来比较这两个对象。我必须使用比较器接口,因为对象存储在Set中,并且在必须对CollectionUtils执行操作之后。我在Google上搜索了一下,发现了Comparator的解决方案,但只有相同的类型。 我试图朝这个方向实施思考,但是我不知道我是否在正

  • 我需要写一个比较器,取一个a类型的对象a和一个B类型的对象B。这两个对象不是一个公共对象的扩展。他们确实是不同的,但我需要比较这两个对象在它的共同领域。我必须使用比较器接口,因为对象存储在Set中,之后我必须使用CollectionUtils进行操作。我搜索了一点点,我用比较器找到了解决方案,但只有相同的类型。 TXS 附注:我在不同的集合中添加两个对象: 之后我会这样想:

  • 问题内容: 我想比较Java中的类类型。 我以为我可以这样做: 我想比较一下是否传递给函数的obj是从MyObject_1扩展而来的。但这是行不通的。似乎getClass()方法和.class提供了不同类型的信息。 如何比较两个类类型,而不必创建另一个伪对象来比较类类型? 问题答案: 试试这个: 由于继承,这对接口也有效: 有关instanceof的更多信息,请访问:http : //mindpr

  • 我有两个对象类Person和Employee。两个类都有共同的属性年龄。我已经在Arraylist中添加了这两个类的几个对象,现在我需要两个,写一个比较器,并将其传递给集合类的sort方法。并希望列表按年龄排序。我尝试这样做只是为了更清楚地使用Java中的carparable和Comparator。 编辑:我问这个问题的原因是我不清楚比较者和可比性。我在某个地方读到,如果类实现了可比较的,那么它就