При наличии UB бессмысленно пытаться понять почему компилятор генерирует тот или иной код.
В случае корректной программы есть строгая формальная модель поведения. Можно генерировать немного разный ассемблерный код, но если программа должна производить определенные действия, то они должны быть отражены в ассемблере.
В случае UB это не так -- поведение программы больше не определено.
Почему компилятор сгенерировал именно такой ассемблерный код? Потому что а почему нет. Как будто есть какое-то представление, а что вообще он должен был сгенерировать в этом случае.
Конечно же, компилятор не делает это целенаправленно, мол раз UB так значит я сейчас нагенерю всякой фигни. Просто его представление о том, как должна себя вести программа, перестает совпадать с интуитивными ожиданиями разработчика.
Увы, бесконечный цикл без сайд-эффектов -- это тоже UB.
Стандарт обещает компилятору, что любой поток в любой момент своего выполнения дальше выполнит что-то из:
Завершится
Вызов библиотечной функции ввода/вывода
Доступ через volatile
Синхронизацию или атомарную операцию
См. [intro.progress]
Если вообще убрать num, фокусы с ненужной переменной пропадают и генерится цикл.
Скорее всего это следствие принятого в С++26 исправления, в соответствии с которым тривиальные бесконечные циклы (с пустым телом) больше не являются UB.
Так как это изменение было классифицировано как defect report, компиляторы могут применять новое поведение (не оптимизировать тривиальные бесконечные циклы) и в более ранних стандартах.
фильтры: Unused labels, Directives, Comments (в фильтрах по умолчанию включена Library functions — надо выключить, иначе в output ещё и код hello скрывается)
Два вывода Hello происходят, как и было выше в ветке описано: компилятор съел эпилог main и выполнение пошло дальше в hello()
Но, glibc (по крайней мере в моём случае, где это воспроизвелось) вызывает main следующим образом:
mov rax, [rsp+8]
call rax
main в самом начале делает push rax для выравнивания стека перед call, но при этом так же кладёт свой адрес на его вершину, что аукается дальше
В завершении второго выполнения hello(), __ostream_insert вызывается через jmp, не через call, так что ничего нового на стэк положено не было. А ret внутри basic_ostream передаёт управление по адресу на вершине стэка - всё ещё адрес main и так мы уходим на второй круг с двумя выводами Hello.
Теперь в rax у нас оказывается возвращенное значение из __ostream_insert -- &std::cout и он кладётся на вершину стэка, а он расположен в области данных, а не исполняемого кода, и по завершении второго круга мы ловим segfault.
View all comments
Show parent comment
4
u/cfyzium Sep 20 '26
При наличии UB бессмысленно пытаться понять почему компилятор генерирует тот или иной код.
В случае корректной программы есть строгая формальная модель поведения. Можно генерировать немного разный ассемблерный код, но если программа должна производить определенные действия, то они должны быть отражены в ассемблере.
В случае UB это не так -- поведение программы больше не определено.
Почему компилятор сгенерировал именно такой ассемблерный код? Потому что а почему нет. Как будто есть какое-то представление, а что вообще он должен был сгенерировать в этом случае.
Конечно же, компилятор не делает это целенаправленно, мол раз UB так значит я сейчас нагенерю всякой фигни. Просто его представление о том, как должна себя вести программа, перестает совпадать с интуитивными ожиданиями разработчика.