瓦尔格林抓起一阵慌乱
条件跳转或移动取决于未初始化的值
在我的一个单元测试中。
在检查程序集时,我发现以下代码:
bool operator==(MyType const& left, MyType const& right) {
// ... some code ...
if (left.getA() != right.getA()) { return false; }
// ... some code ...
return true;
}
在哪里?
MyType::getA() const -> std::optional<std::uint8_t>
,生成了以下程序集:
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
在未设置可选项的情况下不执行(跳过)的语句。
成员
A
这是偏移量
0x1c
进入之内
MyType
. 检查的布局
std::optional
我们看到:
-
+0x1d
对应于
bool _M_engaged
,
-
+0x1c
对应于
std::uint8_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):
-
如果一个选项是
nullopt
另一个被设置,然后条件跳转
+148
跳到
end
(
return false
),好的。
-
如果设置了两个选项,则比较读取初始化值,确定。
所以唯一值得关注的是当两个选项都是
无效选择
:
-
如果值比较相等,则代码得出结论,选项是相等的,这是正确的,因为它们都是
无效选择
,
-
否则,代码得出结论,如果
__lhs._M_engaged
是假的,这是真的。
因此,无论哪种情况,代码都得出结论,当两个选项都是
无效选择
;CQFD。
这是我第一次看到gcc生成明显“良性”的未初始化读取,因此我有几个问题:
-
程序集(x84_64)中的未初始化读取是否正常?
-
这是优化失败的症候群吗(逆转
||
)在非良性情况下,哪种情况会触发?
现在,我倾向于用
optimize(1)
作为防止优化启动的一项工作。幸运的是,所识别的函数不是性能关键的。
环境:
-
编译器:gcc 7.3
-
编译标志:
-std=c++17 -g -Wall -Werror -O3 -flto
(+适当的包括)
-
链接标志:
-O3 -flto
(+适当的库)
注意:可以显示为
-O2
而不是
-O3
,但决不能没有
-flto
.
有趣的事实
在完整代码中,对于各种有效载荷,此模式在上述函数中出现32次:
std::uint8_t
,
std::uint32_t
,
std::uint64_t
甚至
struct { std::int64_t; std::int8_t; }
.
它只出现在几个大的
operator==
比较大约40个数据成员的类型,而不是较小的类型。但它并没有出现在
std::optional<std::string_view>
即使在那些特定的函数中
std::char_traits
作为比较)。
最后,令人恼火的是,将有问题的函数隔离在它自己的二进制文件中会使“问题”消失。神秘的MCVE被证明是难以捉摸的。