Какое соглашение о вызовах использует clang? - PullRequest
0 голосов
/ 22 июня 2019

Какое соглашение о вызовах по умолчанию использует компилятор clang? Я заметил, что когда я возвращаю локальный указатель, ссылка не теряется

#include <stdio.h>

char *retx(void) {
      char buf[4] = "buf";
      return buf;
}

int main(void) {
    char *p1 = retx();
    puts(p1);
    return 0;
}

Ответы [ 2 ]

2 голосов
/ 22 июня 2019

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

1 голос
/ 24 июня 2019

Приложение L к проекту C11 N1570 признает некоторые ситуации (т. Е. «Некритическое неопределенное поведение»), когда Стандарт предъявляет не особые поведенческие требования, а реализации, которые определяют __STDC_ANALYZABLE__ с ненулевым значениемдолжны предлагать некоторые гарантии и другие ситуации («критическое неопределенное поведение»), в которых реализации нередко ничего не гарантируют.Попытки получить доступ к объектам после их срока службы подпадают под последнюю категорию.

Хотя ничто не помешает реализации предоставить поведенческие гарантии, выходящие за рамки требований Стандарта, даже для критического неопределенного поведения, а некоторые задачи требуют, чтобы реализации выполнялипоэтому (например, для многих задач встроенных систем требуется, чтобы программы разыменовывали указатели на адреса, цели которых не удовлетворяют определению для «объектов»), доступ к автоматическим переменным по истечении срока их службы является поведением, о котором лишь немногие реализации могут предложить какие-либо гарантии, помимо, возможно, гарантии того, что чтениепроизвольный адрес ОЗУ не будет иметь побочных эффектов, кроме получения значения Unspecified.

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

Хотя многие реализации могут быть настроены для предоставления полезных гарантий поведения действийдля которого Стандарт не предъявляет никаких требований, я не могу представить какие-либо реализации, которые можно сконфигурировать так, чтобы обеспечить достаточные гарантии, чтобы сделать вышеуказанный код пригодным для использования.

...