更改数据时UB的说明

时间:2013-05-21 11:15:43

标签: c++ const undefined-behavior const-cast

我试图向工作伙伴证明,如果真的想要(并且知道如何)通过使用一些技巧来改变常量限定变量的值,在我的演示中,我发现了存在两种口味"恒定价值观:你无论做什么都无法改变的,以及你可以通过使用肮脏技巧改变的那些。

当编译器使用文字值而不是存储在堆栈中的值(readed here)时,常量值是不可更改的,这里是一个显示内容的piece of code我的意思是:

// TEST 1
#define LOG(index, cv, ncv) std::cout \
    << std::dec << index << ".- Address = " \
    << std::hex << &cv << "\tValue = " << cv << '\n' \
    << std::dec << index << ".- Address = " \
    << std::hex << &ncv << "\tValue = " << ncv << '\n'

const unsigned int const_value = 0xcafe01e;

// Try with no-const reference
unsigned int &no_const_ref = const_cast<unsigned int &>(const_value);
no_const_ref = 0xfabada;
LOG(1, const_value, no_const_ref);

// Try with no-const pointer
unsigned int *no_const_ptr = const_cast<unsigned int *>(&const_value);
*no_const_ptr = 0xb0bada;
LOG(2, const_value, (*no_const_ptr));

// Try with c-style cast
no_const_ptr = (unsigned int *)&const_value;
*no_const_ptr = 0xdeda1;
LOG(3, const_value, (*no_const_ptr));

// Try with memcpy
unsigned int brute_force = 0xba51c;
std::memcpy(no_const_ptr, &brute_force, sizeof(const_value));
LOG(4, const_value, (*no_const_ptr));

// Try with union
union bad_idea
{
    const unsigned int *const_ptr;
    unsigned int *no_const_ptr;
} u;

u.const_ptr = &const_value;
*u.no_const_ptr = 0xbeb1da;
LOG(5, const_value, (*u.no_const_ptr));

这会产生以下输出:

1.- Address = 0xbfffbe2c    Value = cafe01e
1.- Address = 0xbfffbe2c    Value = fabada
2.- Address = 0xbfffbe2c    Value = cafe01e
2.- Address = 0xbfffbe2c    Value = b0bada
3.- Address = 0xbfffbe2c    Value = cafe01e
3.- Address = 0xbfffbe2c    Value = deda1
4.- Address = 0xbfffbe2c    Value = cafe01e
4.- Address = 0xbfffbe2c    Value = ba51c
5.- Address = 0xbfffbe2c    Value = cafe01e
5.- Address = 0xbfffbe2c    Value = beb1da

由于我依赖UB(更改const数据的值),预计程序会很奇怪;但这种怪异比我期待的要多。

让我们假设编译器正在使用文字值,然后,当代码到达指令以更改常量的值时(通过引用,指针或memcpy ing),只需忽略只要值是文字(虽然是未定义的行为)。这解释了为什么价值保持不变但是:

  • 为什么两个变量中的内存地址相同但包含的值不同?

AFAIK相同的内存地址不能指向不同的值,因此,其中一个输出是谎言:

  • 真正发生了什么?哪个内存地址是假的(如果有的话)?

对上面的代码进行一些更改我们可以尝试避免使用文字值,因此欺骗可以完成其工作(source here):

// TEST 2
// Try with no-const reference
void change_with_no_const_ref(const unsigned int &const_value)
{
    unsigned int &no_const_ref = const_cast<unsigned int &>(const_value);
    no_const_ref = 0xfabada;
    LOG(1, const_value, no_const_ref);    
}

// Try with no-const pointer
void change_with_no_const_ptr(const unsigned int &const_value)
{
    unsigned int *no_const_ptr = const_cast<unsigned int *>(&const_value);
    *no_const_ptr = 0xb0bada;
    LOG(2, const_value, (*no_const_ptr));
}

// Try with c-style cast
void change_with_cstyle_cast(const unsigned int &const_value)
{
    unsigned int *no_const_ptr = (unsigned int *)&const_value;
    *no_const_ptr = 0xdeda1;
    LOG(3, const_value, (*no_const_ptr));
}

// Try with memcpy
void change_with_memcpy(const unsigned int &const_value)
{
    unsigned int *no_const_ptr = const_cast<unsigned int *>(&const_value);
    unsigned int brute_force = 0xba51c;
    std::memcpy(no_const_ptr, &brute_force, sizeof(const_value));
    LOG(4, const_value, (*no_const_ptr));
}

void change_with_union(const unsigned int &const_value)
{
    // Try with union
    union bad_idea
    {
        const unsigned int *const_ptr;
        unsigned int *no_const_ptr;
    } u;

    u.const_ptr = &const_value;
    *u.no_const_ptr = 0xbeb1da;
    LOG(5, const_value, (*u.no_const_ptr));
}

int main(int argc, char **argv)
{
    unsigned int value = 0xcafe01e;
    change_with_no_const_ref(value);
    change_with_no_const_ptr(value);
    change_with_cstyle_cast(value);
    change_with_memcpy(value);
    change_with_union(value);

    return 0;
}

产生以下输出:

1.- Address = 0xbff0f5dc    Value = fabada
1.- Address = 0xbff0f5dc    Value = fabada
2.- Address = 0xbff0f5dc    Value = b0bada
2.- Address = 0xbff0f5dc    Value = b0bada
3.- Address = 0xbff0f5dc    Value = deda1
3.- Address = 0xbff0f5dc    Value = deda1
4.- Address = 0xbff0f5dc    Value = ba51c
4.- Address = 0xbff0f5dc    Value = ba51c
5.- Address = 0xbff0f5dc    Value = beb1da
5.- Address = 0xbff0f5dc    Value = beb1da

正如我们所看到的,const-qualified变量在每次change_with_*调用时都被更改,并且行为与之前的相同,除了这个事实,所以我很想假设内存的奇怪行为当const数据用作文字而不是值时,地址清单。

因此,为了确保这一假设,我做了最后一次测试,将unsigned int value中的main更改为const unsigned int value

// TEST 3
const unsigned int value = 0xcafe01e;
change_with_no_const_ref(value);
change_with_no_const_ptr(value);
change_with_cstyle_cast(value);
change_with_memcpy(value);
change_with_union(value);

令人惊讶的是输出与TEST 2code here)相同,所以我认为数据作为变量传递而不是文字值,因为它被用作参数,所以这让我想知道:

  • 什么使编译器决定将const值优化为文字值?

简而言之,我的问题是:

  • TEST 1
    • 为什么const值和no-const值共享相同的内存地址但其包含的值不同?
    • 程序产生此输出的步骤是什么?哪个内存地址是假的(如果有的话)?
  • TEST 3
    • 什么使编译器决定将const值优化为文字值?

1 个答案:

答案 0 :(得分:2)

通常,分析未定义的行为是没有意义的,因为无法保证您可以将分析结果传输到其他程序。

在这种情况下,可以通过假设编译器已应用名为constant propagation的优化技术来解释该行为。在该技术中,如果使用编译器知道值的const变量的值,则编译器将const变量的使用替换为该变量的值(因为它已知)在编译时)。变量的其他用途,例如取其地址,不会被替换。

此优化是有效的,正是因为更改定义为const的变量会导致未定义的行为,并且允许编译器假定程序调用未定义的行为。

因此,在TEST 1中,地址是相同的,因为它是相同的变量,但值不同,因为每对中的第一个反映了编译器假设(正确地)成为的值。变量,第二个反映实际存储的内容。 在TEST 2TEST 3中,编译器无法进行优化,因为编译器无法100%确定函数参数将引用常量值(并且在TEST 2中,它没有)。