The following problem is distilled from a huge project and the most minimal example of the problem I was able to come up with.
I know, deriving from std::string
is bad, and it already is changed in our code base, but I am trying to understand what is happening under the hood here.
The code crashes on Visual C++ 2017
Microsoft Visual Studio Community 2017
Version 15.2 (26430.14) Release
Visual C++ 2017 00369-60000-00001-AA257
in release-mode only (with speed optimization). It does not crash in release-mode without speed optimization.
#include <string>
#include <string_view>
#include <vector>
struct my_string : public std::string
{
__declspec(noinline)
my_string::my_string( const std::string_view& str ) :
std::string( str.data(), str.size() )
{}
template <typename T>
my_string& arg( T )
{
return *this;
}
};
struct my_string_view : public std::string_view
{
my_string_view( const std::string_view::value_type* val ) :
std::string_view( val ) {}
template <typename... PARAMS>
my_string arg( PARAMS&&... prms ) {
return my_string( *this ).arg( std::forward<PARAMS>( prms )... );
}
};
template <typename T>
struct basic_color
{
T r, g, b, a;
basic_color() : r( 0 ), g( 0 ), b( 0 ), a( 255 ) {}
template <typename U>
explicit basic_color( const basic_color<U>& c ) :
r( c.r ), g( c.g ), b( c.b ), a( c.a )
{}
};
using color = basic_color<std::uint8_t>;
using float_color = basic_color<float>;
__declspec(noinline)
void change_float_color( float_color& color )
{
color.r = 0.1f;
}
int main()
{
std::vector<float_color> colors = { {} };
float sum = 0;
for ( std::uint32_t i = 0; i < 1; ++i )
{
float_color fc;
change_float_color( fc );
color c( fc );
std::vector<std::string> msgs;
msgs.push_back( my_string_view( "" ).arg( c.r ) );
msgs.push_back( my_string_view( "" ).arg( c.g ) );
sum += fc.b - colors[i].b;
}
return static_cast<int>(sqrt( sum ));
}
The error in Visual Studio is this (have a look at the broken sizes of msgs
and colors
at the bottom):
My guess is that the call of std::vector<std::string>::push_back(std::string&&)
with a my_string
is problematic (slicing-like behavior). But how can this corrupt the stack (or the stack pointer)?
Does anybody have an idea what could be happening here or how I can find out?
Here is my project in case anyone is interested in reproducing the problem.
I think it is a compiler bug.
Here's what I see from the disassembly: upon main()
entry, esp
is saved into ebx
. At the end, esp
is restored from ebx
. However, in the middle (after calling std::_Destroy_range1
) ebx
value is overwritten with something else. So, at the end, the ret
instruction uses a bogus esp
value, and jumps to an invalid place.
So, actually, the stack isn't corrupted (this bug cannot be catched with data breakpoints as Hans suggested), but the stack pointer is.