Это неопределенное поведение. Это может сработать или не может , в зависимости от того, что компилятор выбрал при компиляции для какой-то конкретной цели. Это буквально un определено, а не "гарантированно сломано"; вот и весь смысл. Компиляторы могут просто полностью игнорировать возможность UB при генерации кода, не используя дополнительные инструкции, чтобы убедиться, что UB сломается. (Если вы хотите это, скомпилируйте с -fsanitize=undefined
).
Чтобы понять, что именно произошло, нужно посмотреть на asm, а не просто попытаться запустить его.
warning: address of stack memory associated with local variable 'buf' returned [-Wreturn-stack-address]
return buf;
^~~
Clang печатает это предупреждение, даже без -Wall
. Именно потому, что это недопустимый C, независимо от того, на какое соглашение о вызовах asm вы нацелены.
Clang использует соглашение о вызовах C для цели, которую он компилирует для 1 . Различные ОС на одном и том же ISA могут иметь разные соглашения, хотя за пределами x86 большинство ISA имеют только одно основное соглашение о вызовах. x86 существовал так долго, что исходные соглашения о вызовах (аргументы стека без регистровых аргументов) были неэффективными, поэтому возникли различные 32-битные соглашения. И Microsoft выбрала другое 64-битное соглашение от всех остальных. Итак, есть x86-64 System V, Windows x64, i386 System V для 32-битной x86, стандартное соглашение AArch64, стандартное соглашение PowerPC и т. Д. И т. Д.
Я несколько раз проверял clang и каждый раз отображал строку
«Решение» / «удача» того, «работает» он или нет, принимается во время компиляции, а не во время выполнения. Компиляция / запуск одного и того же источника несколько раз с одним и тем же компилятором ничего не говорит.
Посмотрите на сгенерированный asm, чтобы узнать, где заканчивается char buf[4]
.
Мое предположение: возможно, вы используете Windows x64 . Работа там более вероятна, чем в большинстве соглашений о вызовах, где можно ожидать, что buf[4]
окажется ниже указателя стека в main
, так что от call
до puts
и puts
будут очень вероятно, что перезаписать его.
Если в Windows x64 компиляция с отключенной оптимизацией, локальный char buf[4]
retx()
может быть помещен в принадлежащее ему теневое пространство. Затем вызывающий абонент вызывает puts()
с тем же выравниванием стека, поэтому теневое пространство retx
становится теневым пространством puts
.
И если puts
произойдет , а не для записи его теневого пространства, то данные в памяти, которые retx
хранятся, все еще там. например возможно puts
- это функция-обертка, которая, в свою очередь, вызывает другую функцию, без предварительной инициализации для себя группы локальных объектов. Но не хвостовой вызов, поэтому он выделяет новое теневое пространство.
(Но это не то, что clang8.0 делает на практике с отключенной оптимизацией. Похоже, что buf[4]
будет помещен ниже RSP и перейдет туда, используя __attribute__((ms_abi))
для получения кода Windows x64 из Linux clang: https://godbolt.org/z/2VszYg)
Но это также возможно в соглашениях об использовании стека, где отступы оставляют для выравнивания указателя стека на 16 перед вызовом. (например, современная i386 System V в Linux для 32-битной x86). puts()
имеет аргумент, но retx()
не имеет, поэтому, возможно, buf[4]
попал в память, которую вызывающий «выделяет» как заполнение, прежде чем нажать указатель arg для puts
.
Конечно, это будет небезопасно, потому что данные будут временно ниже указателя стека в соглашении о вызовах без красной зоны. (Только несколько ABI / соглашений о вызовах имеют красные зоны: память под указателем стека, которая гарантированно не будет засорена асинхронно обработчиками сигналов, обработчиками исключений или отладчиками, вызывающими функции в целевом процессе.)
Мне было интересно, сделает ли оптимизация встроенной и сработает. Но нет, я проверял, что для Windows x64: https://godbolt.org/z/k3xGe4. clang и MSVC оптимизируют удаление любых хранилищ "buf\0"
в память . Вместо этого они просто передают puts
указатель на некоторую неинициализированную память стека.
Код, который ломается с включенной оптимизацией, почти всегда UB.
Сноска 1: За исключением x86-64 System V, где clang использует дополнительную недокументированную «особенность» соглашения о вызовах: узкие целочисленные типы в качестве аргументов функций в регистрах предполагаются расширенными до 32 битов. Оба gcc и clang делают это при вызове, а ICC - нет, поэтому вызов функций clang из скомпилированного кода ICC может привести к поломке. См. Требуется ли расширение знака или нуля при добавлении 32-битного смещения к указателю для ABI x86-64?