在我的一个单元测试中,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>0x1c。检查< code>std::optional的布局,我们看到:成员< code>A
在< code>MyType
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):
因此,唯一感兴趣的情况是当两个选项都是< code>nullopt时:
因此,无论哪种情况,代码都得出结论,当两个选项都为nullopt时,这两个选项是相等的;CQFD公司。
这是我第一次看到gcc生成明显“良性”的未初始化读取,因此我有几个问题:
||逆转)的综合征?目前,我倾向于使用优化(1)
注释少数函数,作为防止优化生效的解决方案。幸运的是,已识别的函数并不是性能关键。
环境:
-std=c 17-g-Wall-Werror-O3-flto
(适当的包含)-O3-flto
(适当的库)注:可以用-O2代替
-O3
出现,但决不能没有
有趣的事实
在完整代码中,此模式在上面概述的函数中出现 32 次,用于各种有效负载:
std::uint8_t
、std::uint32_t
、std::uint64_t
,甚至是结构 { std::int64_t; std::int8_t; }
。
它只出现在几个大
运算符==
中,比较具有~40个数据成员的类型,而不是较小的类型。并且它不会出现在标准::可选
最后,令人愤怒的是,将所讨论的函数隔离在它自己的二进制中会使“问题”消失。事实证明,神话般的MCVE是难以捉摸的。
我不太确定它是由编译器错误引起的。您的代码中可能有一些UB,它允许编译器更积极地优化您的代码。无论如何,对于问题:
在x86 asm中,最糟糕的情况是单个寄存器有一个未知值(或者在可能的内存排序的情况下,您不知道它有两个可能的值,旧的还是新的)。但是如果你的代码不依赖于那个寄存器值,你就很好,不像在C中,C UB意味着你的整个程序在一个有符号整数溢出之后,理论上是完全无用的,甚至在此之前,沿着编译器可以看到的代码路径将会导致UB。在asm中从来没有发生过这样的事情,至少在非特权用户空间代码中没有。
(你可能会做一些事情,通过以奇怪的方式设置控制寄存器或将不一致的东西放入页表或描述符中,从而在内核中导致系统范围内的不可预测行为,但这种情况不会发生,即使你在编译内核代码。)
有些isa具有“不可预测的行为”,如早期ARM。如果对乘法的多个操作数使用同一个寄存器,行为是不可预测的。IDK如果这允许打破管道和破坏其他寄存器,或者如果它仅限于一个意外的乘法结果。我猜是后者。
或者MIPS,如果将分支放在分支延迟槽中,则行为是不可预测的。(由于分支延迟槽,处理异常很混乱...但大概仍然有限制,你不能使机器崩溃或破坏其他进程(在像Unix这样的多用户系统中,如果一个非特权用户空间进程可能会破坏其他用户的任何东西,那将是很糟糕的)。
非常早期的MIPS也有加载延迟槽和乘法延迟槽:你不能在下一条指令中使用加载的结果。如果你过早读取寄存器,你可能会得到寄存器的旧值,或者可能只是垃圾。MIPS=最小互锁管道阶段;他们想把停滞转移给软件,但结果是,当编译器找不到任何有用的东西来处理下一个臃肿的二进制文件时,添加NOP会导致整体代码变慢,而不是在必要时让硬件停滞。但是我们被分支延迟槽困住了,因为删除它们会改变ISA,不像放松对早期软件没有做的事情的限制。
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。 编辑:我问这个问题的原因是我不清楚比较者和可比性。我在某个地方读到,如果类实现了可比较的,那么它就