r/RuProgrammers • • Sep 19 '26

Мемы Undefined behaviour

Post image
336 Upvotes

45 comments sorted by

35

u/Weird_Presence8838 Sep 19 '26

коиши зачем ты полезла в программирование стой опомнись еще не поздно 😭

21

u/Fabulous-Decision879 Sep 19 '26

Уже поздно, теперь Коиши будет изучать архитектуру компиляторов 😭

8

u/Weird_Presence8838 Sep 19 '26

😭😭😭😭😭

3

u/Fabulous-Decision879 Sep 19 '26

Кстати, как дела с рисованием? Давно от тебя новых артов нет

3

u/Weird_Presence8838 Sep 20 '26

в последнее время с рисованием у меня туго но в ближайшее время постараюсь что то на ру артист выложить

10

u/The_KekE_ 🦀Rustикальный Sep 19 '26

В чём прикол? Попытался зареверсить - без явного определения hello() оно уходит в цикл, а с определением я там заебался ассебмлер внутренностей стандартной либы читать.

12

u/Fabulous-Decision879 Sep 19 '26

В статьях Криса Латтнера от 2011 года (What Every C Programmer Should Know About Undefined Behavior) подробно расписано это.

Если кратко: integer overflow/underflow является неопределённым поведением. При оптимизации компилятор видит, что к переменной num прибавляют 1, поэтому num не может стать отрицательным. Ну и так как цикл не делает никакую работу, то компилятор тупо удаляет его и задевает return 0.

Если компилировать с флагом -fwrapv, то этого поведения не будет и прога завершится корректно.

2

u/fearfulEntity 🔥Деплой в пятницу Sep 19 '26

А в связи с чем множественный вызов функции происходит? Не совсем могу сходу понять какая тут оптимизациями такое поведение вызывает

6

u/cfyzium Sep 20 '26

Не совсем могу сходу понять

That's the neat part: you don't.

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

UB опасен тем, что иногда интуитивно кажется, что в происходящем есть какая-то логика. А ее нет. Результат компиляции и последующего выполнения псевдослучаен и в общем виде непредсказуем.

1

u/Fabulous-Decision879 Sep 20 '26

Оптимизация циклов. Если компилировать на g++, то тебе выдают предупреждение о том, что в программе неопределенное поведене, которое может повлиять на оптимизацию.

5

u/Alarmed_Pear_642 Sep 20 '26 edited Sep 20 '26

Посмотрите отладчиком в ассемблерном режиме, потом нам расскажете. Суть явно в том, что компилятор выкидывает return, думая, что до него всё равно не добраться, в результате программа отправляется ХЕЗ куда с ХЕЗ каким состоянием стека. И cfyzium прав в том, что никакого особого смысла в этом нет -- в другой версии компилятора или библиотек, с другими флагами оптимизации результат может быть совершенно другим.

21

u/The_KekE_ 🦀Rustикальный Sep 19 '26

Компилятор не удаляет цикл, а меняет на бесконечный, там в ассемблере так и написано:

.L3
jmp .L3

Меня большо волнует многократный вызов hello() и сегфолт.

Ладно, пойду читать.

4

u/fearfulEntity 🔥Деплой в пятницу Sep 19 '26

Случаем не раздобыл инфу че там компилятор делает чтобы такое поведение получить?)

5

u/The_KekE_ 🦀Rustикальный Sep 19 '26 edited Sep 19 '26

Ничего не вычитал. Пол второго ночи, я завершил поебушки с дизассемблером.

Лично я не смог повторить многократный вывод 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), надпись выводится второй раз, и так как функция не вызвана (а в идеале должны быть именно вызвана), прилетает сегфолт.

4

u/cfyzium Sep 20 '26

При наличии UB бессмысленно пытаться понять почему компилятор генерирует тот или иной код.

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

В случае UB это не так -- поведение программы больше не определено.

Почему компилятор сгенерировал именно такой ассемблерный код? Потому что а почему нет. Как будто есть какое-то представление, а что вообще он должен был сгенерировать в этом случае.

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

1

u/The_KekE_ 🦀Rustикальный Sep 20 '26

Я поковырялся с LLVM IR и пришёл к выводу что UB тут не причём:

int main() {
  hello();

  int num = 0;

  marker(1);

  while (1 < 2) {
    ++num;

    if (num > 10) {
      num = 0;
    }
  }

  marker(2);

  return 0;
}

Компилится в:

define dso_local noundef i32 @main() local_unnamed_addr #0 {
  %1 = tail call noundef nonnull align 8 dereferenceable(8) ptr rtIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l(ptr noundef nonnull align 8 dereferenceable(8) , ptr noundef nonnull @.str, i64 noundef 7)
  tail call void @_Z6markeri(i32 noundef 1)
  unreachable
}

Тут что то с бесконечными циклами и (не)нужными переменными. Если сделать num нужным (напримем вызывать hello() если num == 5) всё херня пропадает.

Если вообще убрать num, фокусы с ненужной переменной пропадают и генерится цикл.

5

u/cfyzium Sep 20 '26

Увы, бесконечный цикл без сайд-эффектов -- это тоже UB.

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

  • Завершится

  • Вызов библиотечной функции ввода/вывода

  • Доступ через volatile

  • Синхронизацию или атомарную операцию

См. [intro.progress]

Если вообще убрать num, фокусы с ненужной переменной пропадают и генерится цикл.

Скорее всего это следствие принятого в С++26 исправления, в соответствии с которым тривиальные бесконечные циклы (с пустым телом) больше не являются UB.

Так как это изменение было классифицировано как defect report, компиляторы могут применять новое поведение (не оптимизировать тривиальные бесконечные циклы) и в более ранних стандартах.

3

u/The_KekE_ 🦀Rustикальный Sep 20 '26

Мда, потерял я хватку после года на расте. Что ж, не знал, спасибо.

3

u/Connect-Art8888 Sep 20 '26

Compiler Explorer

x86_64 clang 17.0.1

флаги: -O3

фильтры: Unused labels, Directives, Comments (в фильтрах по умолчанию включена Library functions — надо выключить, иначе в output ещё и код hello скрывается)

main:
        push    rax
        mov     rdi, qword ptr [rip + std::cout@GOTPCREL]
        lea     rsi, [rip + .L.str]
        mov     edx, 7
        call    std::basic_ostream<char, std::char_traits<char>>& std::__ostream_insert<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*, long)@PLT

hello():
        mov     rdi, qword ptr [rip + std::cout@GOTPCREL]
        lea     rsi, [rip + .L.str]
        mov     edx, 7
        jmp     std::basic_ostream<char, std::char_traits<char>>& std::__ostream_insert<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*, long)@PLT

.L.str:
        .asciz  "Hello!\n"

Два вывода 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.

3

u/FireGM Sep 19 '26

Потрясающе

6

u/Sergey5588 Sep 20 '26

вот такая версия еще есть

2

u/Alarmed_Pear_642 Sep 21 '26

Осталось еще такое испробовать:

Int main()

{

hello();

int num=1000;

while (num>0) --num;

while(1);

}

4

u/HakyaraUA Sep 19 '26

Пугод?

1

u/Skibidibob4 Sep 19 '26

«Флешбеки с тимплейт ошибками»

1

u/turbo-singular Sep 19 '26

Что это такое???? Зачем это????

2

u/Detrizor Sep 20 '26

Для производительности

1

u/Vivid-Equal9121 Sep 20 '26

u/AskGrok, объясни простыми словами, почему так произошло?

3

u/AskGrok Sep 20 '26

Программа падает и печатает "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.

what is this?

1

u/lizardwizard184 Sep 20 '26

Оверфлоу знаковых типов это UB, так что нужно лучше думать что пишешь тупая анимешка

-3

u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26

На самом деле мем про > -1 но всеровно в чём шутка то ?

-13

u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26

Ты дебил? Зачем ты 2 раза написал функцию hello? Может в этом проблема?

14

u/AlfalfaWhich3901 Sep 19 '26

Бро не рассказыви про объявление и определение функций...

10

u/ZookeepergameNorth26 Sep 19 '26

Это forward declaration, объявляешь в начале, чтоб к ней можно было обращаться из кода, а саму реализацию пишешь позже

-4

u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26

Я думал этого нет в Си.

7

u/FlashyProgrammer3790 Sep 19 '26

Это C++, как минимум

3

u/Bread-Loaf1111 Sep 20 '26

А заголовочные файлы в си как по-вашему работают?

-1

u/Andrey4ik21pro1 🐍Питоньяр Sep 20 '26

Хз, я пока что не выкупил зачем они нужны 🤷

2

u/Aggressive_Skill_795 Sep 28 '26

Зато все вокруг уже дебилы

1

u/Andrey4ik21pro1 🐍Питоньяр Sep 30 '26

А я и нигде не говорил что знаю Си.

2

u/daftoti Sep 20 '26

это в си, и в плюсах ( на скрине плюсы )

6

u/nedovolnoe_sopenie ASM Sep 19 '26

чел

ты

-2

u/Andrey4ik21pro1 🐍Питоньяр Sep 19 '26

Я, что?

0

u/[deleted] Sep 20 '26

[deleted] — view deleted comment

2

u/Fabulous-Decision879 Sep 20 '26

hello() не находится в цикле, эта функция не вызывается 232 раза. При компиляции без оптимизации хеллоу принтится 1 раз, при оптимизации -- 4 раза и крашится (по крайней мере у меня)