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
операциями, которые не выполняются (перепрыгиваются) в случае, когда опция НЕ установлена.
Элемент A
здесь находится в смещении 0x1c
в MyType
. Проверка макета std::optional
мы видим, что:
-
+0x1d
соответствуетbool _M_engaged
, -
+0x1c
соответствуетstd::uint8_t _M_payload
(внутри анонимного объединения).
Код интереса для 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):
- Если один параметр имеет значение
nullopt
а другой установлен, то условный переход в+148
перескакивает доend
(return false
), OK. - Если оба параметра установлены, то сравнение считывает инициализированные значения, ОК.
Итак, единственный интересный случай - когда оба nullopt
имеют значение nullopt
:
- если значения сравниваются равными, тогда код заключает, что опции равны, что верно, поскольку они оба являются
nullopt
, - в противном случае, код заключает, что опции равны, если
__lhs._M_engaged
является ложным, что верно.
В любом случае, в этом случае код делает вывод о том, что оба nullopt
равны, когда оба являются nullopt
; CQFD.
Это первый случай, когда я вижу gcc, генерирующий явно "доброкачественные" неинициализированные чтения, и поэтому у меня есть несколько вопросов:
- Неинициализированы, читает OK в сборке (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; }
struct { std::int64_t; std::int8_t; }
struct { std::int64_t; std::int8_t; }
.
Он появляется только в небольшом большом operator==
сравнивая типы с ~ 40 членами данных, а не в меньших. И он не появляется для std::optional<std::string_view>
даже в тех конкретных функциях (которые вызывают в std::char_traits
для сравнения).
Наконец, бесцеремонно, изолируя рассматриваемую функцию в своем собственном двоичном, исчезает "проблема". Мифический MCVE оказывается неуловимым.