Объяснение UB при изменении данных

Я пытался продемонстрировать коллеге по работе, что вы можете изменить значение переменной с постоянной константой, если действительно хотите (и умеете), с помощью некоторой хитрости, во время моей демонстрации я обнаружил, что существует два «аромата» постоянных значений: те, которые вы не можете изменить, что бы вы ни делали, и те, которые вы можете изменить, используя грязные трюки.

Постоянное значение невозможно изменить, когда компилятор использует литеральное значение вместо значения, хранящегося в стеке (прочитанные Вот), вот кусок кода это показывает, что я имею в виду:

// 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 (изменить значение постоянных данных) ожидается, что программа действует странно; но эта странность больше, чем я ожидал.

Предположим, что компилятор использует буквальное значение, а затем, когда код достигает инструкции, чтобы изменить значение константы (по ссылке, указателю или memcpying), просто игнорирует порядок, пока значение является литералом (хотя поведение не определено). Это объясняет, почему значение остается неизменным, но:

  • Почему в обеих переменных один и тот же адрес памяти, но содержащиеся в них значения отличаются?

AFAIK один и тот же адрес памяти не может указывать на разные значения, поэтому один из выходов лежит:

  • Что на самом деле происходит? Какой адрес памяти является поддельным (если есть)?

Сделав несколько изменений в приведенном выше коде, мы можем попытаться избежать использования литерального значения, чтобы хитрость выполнила свою работу (источник здесь):

// 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 была изменена на каждом 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 2 (код здесь), поэтому я предполагаю, что данные передаются как переменные, а не как буквальные значения из-за их использования в качестве параметра, поэтому меня удивляет:

  • Что заставляет компилятор решить оптимизировать константное значение как буквальное значение?

Вкратце, мои вопросы:

  • В TEST 1,
    • Почему константное значение и неконстантное значение имеют один и тот же адрес памяти, но его содержащее значение отличается?
    • Какие шаги следует программе для получения этого вывода? Какой адрес памяти является поддельным (если есть)?
  • В TEST 3
    • Что заставляет компилятор решить оптимизировать константное значение как буквальное значение?

0

Решение

В общем, анализировать неопределенное поведение бессмысленно, потому что нет никакой гарантии, что вы можете перенести результаты своего анализа в другую программу.

В этом случае поведение можно объяснить, если предположить, что компилятор применил метод оптимизации, называемый постоянное распространение. В этой технике, если вы используете значение const переменная, для которой компилятор знает значение, то компилятор заменяет использование const переменная со значением этой переменной (как это известно во время компиляции). Другие варианты использования переменной, такие как получение ее адреса, не заменяются.

Эта оптимизация действительна именно потому, что изменение переменной, которая была определена как const приводит к неопределенному поведению, и компилятор может предполагать, что программа не вызвать неопределенное поведение.

Итак, в TEST 1адреса одинаковы, потому что это все одна и та же переменная, но значения отличаются, потому что первая из каждой пары отражает то, что компилятор предполагает (справедливо), чтобы быть значением переменной, а вторая отражает то, что на самом деле там хранится.
В TEST 2 а также TEST 3компилятор не может выполнить оптимизацию, потому что компилятор не может быть на 100% уверен, что аргумент функции будет ссылаться на постоянное значение (и в TEST 2нет)

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]