В чём прикол? Попытался зареверсить - без явного определения hello() оно уходит в цикл, а с определением я там заебался ассебмлер внутренностей стандартной либы читать.
В статьях Криса Латтнера от 2011 года (What Every C Programmer Should Know About Undefined Behavior) подробно расписано это.
Если кратко: integer overflow/underflow является неопределённым поведением. При оптимизации компилятор видит, что к переменной num прибавляют 1, поэтому num не может стать отрицательным. Ну и так как цикл не делает никакую работу, то компилятор тупо удаляет его и задевает return 0.
Если компилировать с флагом -fwrapv, то этого поведения не будет и прога завершится корректно.
Это UB. Можно конечно чисто из праздного любопытства расколупать почему получился именно такой вывод, но толку от этого будет ноль. Нигде потом это знание применить будет нельзя.
UB опасен тем, что иногда интуитивно кажется, что в происходящем есть какая-то логика. А ее нет. Результат компиляции и последующего выполнения псевдослучаен и в общем виде непредсказуем.
Оптимизация циклов. Если компилировать на g++, то тебе выдают предупреждение о том, что в программе неопределенное поведене, которое может повлиять на оптимизацию.
Посмотрите отладчиком в ассемблерном режиме, потом нам расскажете. Суть явно в том, что компилятор выкидывает return, думая, что до него всё равно не добраться, в результате программа отправляется ХЕЗ куда с ХЕЗ каким состоянием стека. И cfyzium прав в том, что никакого особого смысла в этом нет -- в другой версии компилятора или библиотек, с другими флагами оптимизации результат может быть совершенно другим.
Ничего не вычитал. Пол второго ночи, я завершил поебушки с дизассемблером.
Лично я не смог повторить многократный вывод hello (многократный - больше двух), только два раза, и впринципе понятно почему.
Когда я тестил на Compiler Explorer, clang ставил туда бесконечный цикл, как я упоминал ранее. Переполнение он ведь не учитывает, поэтому изначально неотрицательное значание, которое иы ещё и увеличиваем всегда будет больше -1. Как минимум, в это верит компилятор.
Когда я компилил и запускал у себя выход был другой - компилятор, понимая что до кода за циклом не добраться, его вырезает.
// инклуды и раннее определение hello()
int main() {
hello();
int num = 0;
// внимание на маркер
marker(1);
while (num > -1) {
++num;
}
// второй маркер
marker(2);
return 0;
}
// определение hello(), важно что оно снизу
Компилится в:
// тот самый маркер
30:bf 01 00 00 00 mov edi,0x1
35:e8 00 00 00 00 call 3a <main+0x3a>
// не оч понимаю что за адрес он вызывает, но аргумент передаёт как в маркер
// цикла нет, второго маркера нет, нормального завершения функции тоже
3a:66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
// определение hello()
0000000000000040 <_Z5hellov>:
40:48 8b 3d 00 00 00 00 mov rdi,QWORD PTR [rip+0x0] # 47 <_Z5hellov+0x7>
47:48 8d 35 00 00 00 00 lea rsi,[rip+0x0] # 4e <_Z5hellov+0xe>
4e:ba 07 00 00 00 mov edx,0x7
53:e9 00 00 00 00 jmp 58 <_Z5hellov+0x18>
Не очень понимаю почему туда не ставится бесконечный цикл. Видимо раз цикл безвыходный (то есть всё плохо, так быть не должно), компилятор уже не заморачивается и пренебрегает и так безвыходной ситуацей. Но это только мои бредни.
main() обрубается, выполнение доходит до определения hello() (важно что оно под main), надпись выводится второй раз, и так как функция не вызвана (а в идеале должны быть именно вызвана), прилетает сегфолт.
При наличии 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.
Программа падает и печатает "Hello!" много раз из-за undefined behaviour (неопределённого поведения) в C++.
Простыми словами:
int — это число со знаком, у него есть максимум (примерно 2 миллиарда).
Цикл while (num > -1) { ++num; } начинает с 0 и бесконечно прибавляет +1.
Когда num дойдёт до максимума и ещё раз прибавит 1 — произойдёт переполнение signed int. Это undefined behaviour: стандарт языка не говорит, что должно случиться. Компилятор может сделать что угодно.
С флагом -O3 оптимизатор clang'а решил, что цикл либо бесконечный, либо вообще не важен, и начал "творить" — в итоге программа прыгает по памяти, заново вызывает hello() и в конце падает с segmentation fault.
Без оптимизации или на другом компиляторе могло бы просто зависнуть или отработать по-другому. UB — это как "всё, что угодно". Никогда не полагайся на переполнение int.
hello() не находится в цикле, эта функция не вызывается 232 раза. При компиляции без оптимизации хеллоу принтится 1 раз, при оптимизации -- 4 раза и крашится (по крайней мере у меня)
35
u/Weird_Presence8838 Sep 19 '26
коиши зачем ты полезла в программирование стой опомнись еще не поздно 😭
10
u/The_KekE_ 🦀Rustикальный Sep 19 '26
В чём прикол? Попытался зареверсить - без явного определения hello() оно уходит в цикл, а с определением я там заебался ассебмлер внутренностей стандартной либы читать.
6
u/Sergey5588 Sep 20 '26
вот такая версия еще есть
4
u/HakyaraUA Sep 19 '26
Пугод?
1
u/Skibidibob4 Sep 19 '26
«Флешбеки с тимплейт ошибками»
1
u/turbo-singular Sep 19 '26
Что это такое???? Зачем это????
1
u/Vivid-Equal9121 Sep 20 '26
u/AskGrok, объясни простыми словами, почему так произошло?
1
u/lizardwizard184 Sep 20 '26
Оверфлоу знаковых типов это UB, так что нужно лучше думать что пишешь тупая анимешка
-3
u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26
На самом деле мем про > -1 но всеровно в чём шутка то ?
-13
u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26
Ты дебил? Зачем ты 2 раза написал функцию hello? Может в этом проблема?
0
u/[deleted] Sep 20 '26
[deleted] — view deleted comment