c++ - 用于比较原始类型的 std::optional 的有趣程序集

标签 c++ gcc assembly x86-64 c++17

Valgrind 在我的一个单元测试中快速有条件的跳转或移动取决于未初始化的值

检查程序集,我意识到以下代码:

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 标记的地方在未设置可选的情况下不执行(跳过)的语句。

成员(member)A这里是偏移量 0x1c进入 MyType .检查 std::optional 的布局我们看到:

  • +0x1d对应于 bool _M_engaged ,
  • +0x1c对应于 std::uint8_t _M_payload (在匿名 union 内)。

std::optional 的感兴趣代码是:

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):

  1. 如果其中一项是 nullopt另一个是设置,然后条件跳转+148跳转到 end (return false),好的。
  2. 如果两个选项都设置了,那么比较读取初始化值,OK。

所以唯一感兴趣的情况是两个选项都是 nullopt :

  • 如果值比较相等,则代码得出结论,可选值相等,这是真的,因为它们都是 nullopt ,
  • 否则,如果 __lhs._M_engaged,则代码得出的结论是可选值相等。是假的,这是真的。

在任何一种情况下,代码都会得出结论,当两个选项都是 nullopt 时,两个选项是相等的。 ; CQFD。


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

  1. 在汇编 (x84_64) 中未初始化的读取是否正常?
  2. 这是在非良性情况下可能触发的优化失败综合症(反转 ||)吗?

目前,我倾向于使用 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 被证明是难以捉摸的。

最佳答案

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

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

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

关于c++ - 用于比较原始类型的 std::optional 的有趣程序集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51616179/

相关文章:

assembly - MSP430 组装说明

linux - Nasm 代码在 windows 上运行但在 linux 上不运行

c++ - 你如何传递 std::array?

c++ - 如何将 INT64 写入 CString

linux - 链接汇编子例程未按预期工作

android - 跨原生 GCC 4.8 构建 : libcpp Error: invalid conversion from long long to off_t (aka long int)

c - 为什么我的 scipy 构建失败了?

c++ - 转换字符串大小写的函数?

c++ - 扁平化二维 vector 的通用方法

c++ - 在使用特定标志时添加#define