Почему использование alloca () не считается хорошей практикой?


401

alloca()выделяет память в стеке, а не в куче, как в случае malloc(). Итак, когда я возвращаюсь из рутины, память освобождается. На самом деле это решает мою проблему освобождения динамически выделяемой памяти. Освобождение памяти, выделенной через malloc()это, является большой головной болью и, если что-то пропущено, приводит к всевозможным проблемам с памятью.

Почему использование не alloca()рекомендуется, несмотря на вышеуказанные функции?


40
Просто быстрая заметка. Хотя эту функцию можно найти в большинстве компиляторов, она не требуется стандартом ANSI-C и поэтому может ограничивать переносимость. Другое дело, что вы не должны! free () указатель, который вы получаете, и он автоматически освобождается после выхода из функции.
Меркуро

9
Кроме того, функция с alloca () не будет встроена, если объявлена ​​как таковая.
Юстиция

2
@ Справедливость, можешь дать какое-нибудь объяснение? Мне очень любопытно, что стоит за этим поведением
migajek

47
Забудьте о шуме переносимости, не нужно вызывать free(что, очевидно, является преимуществом), неспособность встроить его (очевидно, что выделение кучи намного тяжелее) и т. Д. Единственная причина, по которой следует избегать, alloca- большие размеры. То есть тратить тонны стековой памяти не очень хорошая идея, плюс у вас есть шанс переполнения стека. Если это так - рассмотрите возможность использования malloca/freea
valdo

5
Другим положительным моментом allocaявляется то, что стек не может быть фрагментирован как куча. Это может оказаться полезным для сложных приложений, работающих в режиме реального времени, или даже критически важных для безопасности приложений, поскольку WCRU может быть подвергнут статическому анализу, не прибегая к пользовательским пулам памяти с их собственным набором проблем (отсутствие временной локализации, неоптимальный ресурс использование).
Андреас

Ответы:


245

Ответ прямо на manстранице (по крайней мере, в Linux ):

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ Функция alloca () возвращает указатель на начало выделенного пространства. Если распределение вызывает переполнение стека, поведение программы не определено.

Что не означает, что его никогда не следует использовать. Один из проектов OSS, над которым я работаю, широко использует его, и если вы не злоупотребляете им ( allocaимея огромные ценности), это нормально. Как только вы пройдете отметку «несколько сотен байт», пришло время использовать mallocи друзей, вместо этого. Вы все еще можете получить ошибки выделения, но, по крайней мере, у вас будет некоторое указание на ошибку, а не просто выброс стека.


35
Таким образом, на самом деле нет никаких проблем с этим, которые вы бы не имели при объявлении больших массивов?
TED

88
@Sean: Да, риск переполнения стека является причиной, но эта причина немного глупа. Во-первых, потому что (как говорит Вайбхав), большие локальные массивы вызывают точно такую ​​же проблему, но не так сильно очерчены. Кроме того, рекурсия может так же легко взорвать стек. Извините, но я надеюсь, что вы опровергаете преобладающую идею о том, что причина, приведенная на странице руководства, оправдана.
j_random_hacker

49
Я хочу сказать, что обоснование, приведенное на странице руководства, не имеет смысла, так как alloca () точно так же «плоха», как и другие вещи (локальные массивы или рекурсивные функции), которые считаются кошерными.
j_random_hacker

39
@ninjalj: Не очень опытные программисты на C / C ++, но я думаю, что многие люди, которые боятся alloca(), не имеют такого же страха перед локальными массивами или рекурсией (на самом деле многие люди, которые будут кричать, alloca()будут хвалить рекурсию, потому что она «выглядит элегантно») , Я согласен с советом Шона («alloca () подходит для небольших распределений»), но я не согласен с тем, что фреймы alloca () являются уникально злыми среди 3 - они одинаково опасны!
j_random_hacker

35
Примечание. Учитывая «оптимистичную» стратегию распределения памяти в Linux, вы, скорее всего , не получите никаких признаков сбоя при исчерпании кучи ... вместо этого malloc () вернет вам хороший не-NULL-указатель, а затем, когда вы попытаетесь на самом деле доступ к адресному пространству, на которое он указывает, ваш процесс (или какой-то другой процесс, непредсказуемо) будет убит OOM-убийцей. Конечно, это «особенность» Linux, а не проблема C / C ++ как таковая, но об этом следует помнить при обсуждении вопроса о том, «безопаснее» ли alloca () или malloc (). :)
Джереми Фризнер,

209

Одна из самых запоминающихся ошибок, которые у меня были, заключалась в использовании встроенной функции alloca. Это проявилось как переполнение стека (поскольку оно размещается в стеке) в случайных точках выполнения программы.

В заголовочном файле:

void DoSomething() {
   wchar_t* pStr = alloca(100);
   //......
}

В файле реализации:

void Process() {
   for (i = 0; i < 1000000; i++) {
     DoSomething();
   }
}

То, что произошло, было встроенной DoSomethingфункцией компилятора, и все выделения стека происходили внутри Process()функции и, таким образом, взрывали стек. В свою защиту (и я не был тем, кто обнаружил проблему; мне пришлось пойти и поплакаться с одним из старших разработчиков, когда я не мог это исправить), это было не прямо alloca, это было одно из преобразования строки ATL макросы.

Итак, урок - не используйте allocaфункции, которые, по вашему мнению, могут быть встроены.


91
Интересно. Но разве это не квалифицируется как ошибка компилятора? В конце концов, встраивание изменило поведение кода (оно задержало освобождение пространства, выделенного с помощью alloca).
слеске

60
По-видимому, по крайней мере GCC примет это во внимание: «Обратите внимание, что некоторые использования в определении функции могут сделать его непригодным для встроенной замены. Среди этих применений: использование varargs, использование alloca, [...]». gcc.gnu.org/onlinedocs/gcc/Inline.html
sleske 17.10.10

139
Какой компилятор ты курил?
Томас Эдинг

22
Что я не понимаю, так это то, почему компилятор не использует область видимости для определения того, что allocas в подскопе более или менее «освобожден»: указатель стека может вернуться к своей точке перед входом в область действия, как, например, когда возвращение из функции (не так ли?)
моала

8
Я понизил голос, но ответ хорошо написан: я согласен с другими, что вы ошибаетесь в alloca за явную ошибку компилятора . Компилятор сделал ошибочное предположение при оптимизации, которую он не должен был делать. Обойти ошибку компилятора - это хорошо, но я бы не стал винить в этом ничего, кроме компилятора.
Эван Кэрролл

75

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

char arr[size];

вместо

char *arr=alloca(size);

Он находится в стандарте C99 и существовал как расширение компилятора во многих компиляторах.


5
Это упомянуто Джонатаном Леффлером в комментарии к ответу Артура Ульфельда.
ниндзя

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

6
Одно замечание - это массивы переменной длины, а не динамические. Последние имеют изменяемый размер и обычно реализуются в куче.
Тим Час

1
Visual Studio 2015 при компиляции некоторых C ++ имеет ту же проблему.
АХКОКС

2
Линус Торвальдс не любит VLA в ядре Linux . Начиная с версии 4.20 Linux должен быть почти без VLA.
Кристиан Чиупиту

60

alloca () очень полезна, если вы не можете использовать стандартную локальную переменную, потому что ее размер должен быть определен во время выполнения, и вы можете абсолютно гарантировать, что указатель, полученный из alloca (), НИКОГДА не будет использоваться после возврата этой функции .

Вы можете быть в полной безопасности, если вы

  • не возвращайте указатель или что-либо, что содержит его.
  • не храните указатель в какой-либо структуре, размещенной в куче
  • не позволяйте никаким другим потокам использовать указатель

Настоящая опасность исходит из того, что кто-то еще нарушит эти условия спустя некоторое время. Имея это в виду, он отлично подходит для передачи буферов в функции, которые форматируют текст в них :)


12
Функция VLA (массив переменной длины) в C99 поддерживает динамические размеры локальных переменных, не требуя явного использования alloca ().
Джонатан Леффлер

2
Neato! Более подробную информацию можно найти в разделе «3.4 Массив переменной длины» по
Артур Ульфельдт,

1
Но это не отличается от работы с указателями на локальные переменные. Их тоже можно одурачить ...
glglgl

2
@Jonathan Leffler. Одна вещь, которую вы можете сделать с alloca, но вы не можете сделать с VLA, это использовать ключевое слово restrict с ними. Например: float * restrictantly_used_arr = alloca (sizeof (float) * size); вместо того, чтобы плавать heavyly_used_arr [размер]. Это может помочь некоторым компиляторам (в моем случае gcc 4.8) оптимизировать сборку, даже если размер является константой компиляции. См. Мой вопрос об этом: stackoverflow.com/questions/19026643/using-restrict-with-arrays
Петр Лопусевич

@JonathanLeffler VLA является локальным по отношению к блоку, который его содержит. С другой стороны, alloca()выделяет память, которая длится до конца функции. Это означает, что не существует простого и удобного перевода на VLA f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }. Если вы считаете, что можно автоматически переводить варианты allocaиспользования VLA в использование, но для описания того, как это сделать, нужно больше, чем просто комментарий, я могу задать этот вопрос.
Паскаль Куок

40

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

  • Не все компиляторы поддерживают alloca.
  • Некоторые компиляторы по-разному интерпретируют предполагаемое поведение alloca, поэтому переносимость не гарантируется даже между компиляторами, которые его поддерживают.
  • Некоторые реализации содержат ошибки.

24
Одна вещь, о которой я упоминал в этой ссылке, которой нет в других разделах этой страницы, заключается в том, что для функции, которая использует alloca()отдельные регистры, для хранения указателя стека и указателя кадра. На процессорах x86> = 386 указатель стека ESPможет использоваться для обоих, освобождая EBP- если alloca()не используется.
j_random_hacker

10
Другим хорошим моментом на этой странице является то, что, если генератор кода компилятора не будет обрабатывать его как особый случай, f(42, alloca(10), 43);может произойти сбой из-за вероятности того, что указатель стека будет изменен alloca() после того, как на него будет передан хотя бы один из аргументов.
j_random_hacker

3
Связанный пост, кажется, написан Джоном Левайном - чуваком, который написал «Линкеры и загрузчики», я бы поверил всему, что он скажет.
user318904

3
Связанный пост является ответом на пост Джона Левина.
А. Уилкокс

6
Имейте в виду, что многое изменилось с 1991 года. Все современные компиляторы C (даже в 2009 году) должны обрабатывать alloca как особый случай; это встроенная, а не обычная функция, и может даже не вызывать функцию. Таким образом, проблема с параметром alloca (которая возникла в K & R C с 1970-х годов) не должна быть проблемой в настоящее время. Более подробно в комментарии, который я сделал к ответу Тони
Ди

26

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


21

все-таки использование alloca не рекомендуется, почему?

Я не воспринимаю такой консенсус. Много сильных плюсов; несколько минусов:

  • C99 предоставляет массивы переменной длины, которые часто используются преимущественно, поскольку обозначения более соответствуют массивам фиксированной длины и в целом интуитивно понятны
  • многие системы имеют меньше общей памяти / адресного пространства, доступного для стека, чем для кучи, что делает программу несколько более восприимчивой к исчерпанию памяти (через переполнение стека): это может рассматриваться как хорошая или плохая вещь - один из-за того, что стек автоматически не увеличивается, как это делает куча, заключается в том, чтобы предотвратить неконтролируемые программы, оказывающие столь же негативное влияние на всю машину
  • при использовании в более локальной области (например, в цикле whileили for) или в нескольких областях область памяти накапливается за одну итерацию / область и не освобождается до выхода из функции: это контрастирует с обычными переменными, определенными в области структуры управления (например, for {int i = 0; i < 2; ++i) { X }будет накапливать alloca-ed память, запрошенную в X, но память для массива фиксированного размера будет перерабатываться за итерацию).
  • современные компиляторы, как правило, не inlineвызывают функции, которые вызываются alloca, но если вы их принудительно вызовите , allocaэто произойдет в контексте вызывающего (т. е. стек не будет освобожден до тех пор, пока вызывающий не вернется)
  • давно allocaперешел от непереносимой функции / хака к стандартизированному расширению, но некоторое негативное восприятие может сохраняться
  • время жизни связано с областью действия функции, которая может или не может подходить программисту лучше, чем mallocявный контроль
  • необходимость использования mallocзаставляет задуматься о освобождении - если это осуществляется с помощью функции-оболочки (например WonderfulObject_DestructorFree(ptr)), то эта функция обеспечивает точку для операций очистки реализации (таких как закрытие файловых дескрипторов, освобождение внутренних указателей или выполнение некоторых журналов) без явных изменений в клиенте код: иногда это хорошая модель для последовательного принятия
    • в этом псевдо-ОО-стиле программирования естественно хотеть чего-то подобного WonderfulObject* p = WonderfulObject_AllocConstructor();- это возможно, когда «конструктор» - это функция, возвращающая malloc-ed-память (так как память остается выделенной после того, как функция возвращает значение, которое будет сохранено p), но не если «конструктор» используетalloca
      • Макро-версия WonderfulObject_AllocConstructorмогла бы достичь этого, но «макросы - это зло» в том смысле, что они могут конфликтовать друг с другом и не-макро-кодом и создавать непреднамеренные замены и, как следствие, трудно диагностируемые проблемы.
    • пропущенные freeоперации могут быть обнаружены ValGrind, Purify и т. д., но пропущенные вызовы «деструктор» не всегда могут быть обнаружены вообще - одно очень незначительное преимущество с точки зрения принудительного использования по назначению; в некоторых alloca()реализациях (например, GCC) используется встроенный макрос alloca(), поэтому замена библиотеки времени использования памяти во время выполнения невозможна, как для malloc/ realloc/ free(например, электрический забор)
  • у некоторых реализаций есть тонкие проблемы: например, из man-страницы Linux:

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


Я знаю, что этот вопрос помечен C, но, как программист C ++, я подумал, что буду использовать C ++, чтобы проиллюстрировать потенциальную полезность alloca: приведенный ниже код (и здесь на ideone ) создает вектор, отслеживающий полиморфные типы разного размера, которые выделяются в стеке (с помощью время жизни привязано к функции return), а не кучи.

#include <alloca.h>
#include <iostream>
#include <vector>

struct Base
{
    virtual ~Base() { }
    virtual int to_int() const = 0;
};

struct Integer : Base
{
    Integer(int n) : n_(n) { }
    int to_int() const { return n_; }
    int n_;
};

struct Double : Base
{
    Double(double n) : n_(n) { }
    int to_int() const { return -n_; }
    double n_;
};

inline Base* factory(double d) __attribute__((always_inline));

inline Base* factory(double d)
{
    if ((double)(int)d != d)
        return new (alloca(sizeof(Double))) Double(d);
    else
        return new (alloca(sizeof(Integer))) Integer(d);
}

int main()
{
    std::vector<Base*> numbers;
    numbers.push_back(factory(29.3));
    numbers.push_back(factory(29));
    numbers.push_back(factory(7.1));
    numbers.push_back(factory(2));
    numbers.push_back(factory(231.0));
    for (std::vector<Base*>::const_iterator i = numbers.begin();
         i != numbers.end(); ++i)
    {
        std::cout << *i << ' ' << (*i)->to_int() << '\n';
        (*i)->~Base();   // optionally / else Undefined Behaviour iff the
                         // program depends on side effects of destructor
    }
}

нет +1 из-за рыбного идиосинкразического способа обработки нескольких типов :-(
einpoklum

@einpoklum: ну, это очень поучительно ... спасибо.
Тони Делрой

1
Позвольте мне перефразировать: это очень хороший ответ. Вплоть до того момента, когда я думаю, что вы предлагаете людям использовать своего рода контр-шаблон.
einpoklum

Комментарий с man-страницы linux очень старый и, я уверен, устарел. Все современные компиляторы знают, что такое alloca (), и не будут так ломаться. В старом K & R C, (1) все функции использовали указатели кадра (2) Все вызовы функций были {push args on stack} {call func} {add # n, sp}. alloca была функцией lib, которая просто увеличивала стек, компилятор даже не знал об этом. (1) и (2) больше не соответствуют действительности, поэтому alloca не может работать таким образом (теперь это присуще). В старом C вызовы alloca в середине нажатия аргументов, очевидно, тоже нарушили бы эти предположения.
Грегго

4
Что касается примера, я бы вообще был обеспокоен тем, что требует Always_inline, чтобы избежать повреждения памяти ....
greggo

14

Все остальные ответы верны. Однако, если то, что вы хотите выделить с помощью alloca(), достаточно мало, я думаю, что это хорошая техника, которая быстрее и удобнее, чем использование malloc()или иное.

Другими словами, alloca( 0x00ffffff )это опасно и может вызвать переполнение, ровно столько, сколько char hugeArray[ 0x00ffffff ];есть. Будьте осторожны и разумны, и все будет в порядке.


12

Много интересных ответов на этот «старый» вопрос, даже некоторые относительно новые ответы, но я не нашел ни одного, кто бы упомянул об этом ....

При правильном и осторожном использовании последовательное использование alloca() (возможно, в масштабах всего приложения) для обработки небольших распределений переменной длины (или VLA C99, если они доступны) может привести к более низкому общему росту стека, чем эквивалентная реализация, использующая локальные массивы увеличенной длины фиксированной длины. , Так что alloca()может быть полезно для вашего стека, если вы используете его осторожно.

Я нашел эту цитату в .... Хорошо, я сделал эту цитату. Но на самом деле, подумай об этом ....

@j_random_hacker очень прав в своих комментариях к другим ответам: отказ от использования alloca()локальных массивов в пользу негабаритных не делает вашу программу более защищенной от переполнения стека (если ваш компилятор не достаточно стар, чтобы разрешить встраивание функций, которые используют, alloca()в этом случае вам следует обновить, или если вы не используете alloca()внутренние циклы, в этом случае вы не должны ... использовать alloca()внутренние циклы).

Я работал на настольных / серверных средах и встроенных системах. Многие встраиваемые системы вообще не используют кучу (они даже не ссылаются на ее поддержку) по причинам, которые включают в себя восприятие, что динамически выделяемая память является злом из-за риска утечек памяти в приложении, которое никогда не будет когда-либо перезагружается годами или более разумным обоснованием того, что динамическая память опасна, поскольку невозможно точно знать, что приложение никогда не фрагментирует свою кучу до точки ложного исчерпания памяти. Таким образом, у встроенных программистов остается мало альтернатив.

alloca() (или VLA) может быть просто правильным инструментом для работы.

Я снова и снова видел, как программист делает выделенный стеком буфер, «достаточно большой, чтобы справиться с любым возможным случаем». В глубоко вложенном дереве вызовов повторное использование этого (анти -?) Шаблона приводит к чрезмерному использованию стека. (Представьте себе дерево вызовов глубиной 20 уровней, где на каждом уровне по разным причинам функция слепо перераспределяет буфер в 1024 байта «просто для безопасности», когда обычно она использует только 16 или менее из них, и только в очень в редких случаях можно использовать больше.) Альтернативой является использованиеalloca()или VLA, и выделяйте столько стекового пространства, сколько требуется вашей функции, чтобы избежать ненужной нагрузки на стек. Надеемся, что когда одна функция в дереве вызовов нуждается в распределении, превышающем нормальное, другие в дереве вызова все еще используют свои обычные небольшие распределения, и общее использование стека приложения значительно меньше, чем если бы каждая функция слепо перераспределяла локальный буфер ,

Но если вы решите использовать alloca()...

Основываясь на других ответах на этой странице, кажется, что VLA должны быть безопасными (они не объединяют распределения стека при вызове из цикла), но если вы используете alloca(), будьте осторожны, чтобы не использовать его внутри цикла, и сделайте убедитесь, что ваша функция не может быть встроенной, если есть вероятность, что она может быть вызвана в цикле другой функции.


Я согласен с этим пунктом. Опасных alloca()верно, но то же самое можно сказать и утечек памяти с malloc()(почему бы не использовать GC тогда? Можно утверждать). alloca()при осторожном использовании может быть очень полезно уменьшить размер стека.
Фелипе Тонелло

Еще одна веская причина не использовать динамическую память, особенно встроенную: это сложнее, чем придерживаться стека. Использование динамической памяти требует специальных процедур и структур данных, тогда как для стека это (для упрощения) вопрос сложения / вычитания большего числа из стека указателя.
18:18

Sidenote: Пример «использования фиксированного буфера [MAX_SIZE]» показывает, почему политика избыточной памяти работает так хорошо. Программы выделяют память, к которой они могут никогда не прикасаться, кроме как в пределах длины их буфера. Поэтому хорошо, что Linux (и другие ОС) на самом деле не назначают страницу памяти до ее первого использования (в отличие от malloc'd). Если размер буфера превышает одну страницу, программа может использовать только первую страницу и не тратить оставшуюся физическую память.
Katastic Voyage

@KatasticVoyage Если MAX_SIZE не больше (или не меньше) размера страницы виртуальной памяти вашей системы, ваш аргумент не выдерживает критики. Также во встроенных системах без виртуальной памяти (многие встроенные микроконтроллеры не имеют MMU), политика избыточной памяти может быть полезной с точки зрения «убедитесь, что ваша программа будет работать в любых ситуациях», но эта уверенность приходит с ценой, что размер вашего стека должно быть также выделено для поддержки этой политики избыточной памяти. В некоторых встраиваемых системах это цена, которую некоторые производители недорогих продуктов не готовы платить.
телефонная галочка

11

Все уже указали на большую вещь, которая заключается в потенциальном неопределенном поведении из-за переполнения стека, но я должен отметить, что в среде Windows есть отличный механизм, чтобы уловить это, используя структурированные исключения (SEH) и защитные страницы. Поскольку стек увеличивается только по мере необходимости, эти защитные страницы находятся в незанятых областях. Если вы выделяете их (переполняя стек), создается исключение.

Вы можете перехватить это исключение SEH и вызвать _resetstkoflw, чтобы сбросить стек и продолжить свой веселый путь. Это не идеально, но это еще один механизм, по крайней мере, знать, что что-то пошло не так, когда вещи попадают в фанат. * У nix может быть что-то похожее, чего я не знаю.

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

Кроме того, помимо предотвращения утечек памяти, alloca не вызывает фрагментацию памяти, что очень важно. Я не думаю, что alloca - плохая практика, если вы используете ее разумно, что в принципе верно для всего. :-)


Проблема в том, что это alloca()может потребовать столько места, что указатель стека попадает в кучу. При этом злоумышленник, который может контролировать размер alloca()и данные, поступающие в этот буфер, может перезаписать кучу (что очень плохо).
12431234123412341234123

SEH - вещь только для Windows. Это замечательно, если вы заботитесь только о своем коде, работающем в Windows, но если ваш код должен быть кроссплатформенным (или если вы пишете код, который работает только на платформе не-Windows), то вы не можете полагаться на SEH.
Джордж

10

alloca () хороша и эффективна ... но она также глубоко нарушена.

  • неправильное поведение области (область функции вместо области блока)
  • использовать несовместимо с malloc ( указатель на alloca () не должен освобождаться, поэтому вы должны отслеживать, откуда приходят ваши указатели на free (), только те, которые вы получили с помощью malloc () )
  • плохое поведение, когда вы также используете встраивание (область видимости иногда переходит к функции вызывающей стороны в зависимости от того, вставлен ли вызываемый объект или нет).
  • нет проверки границы стека
  • неопределенное поведение в случае сбоя (не возвращает NULL как malloc ... и что означает сбой, так как он все равно не проверяет границы стека ...)
  • не стандарт

В большинстве случаев вы можете заменить его, используя локальные переменные и размер мажоранты. Если это используется для больших объектов, то поместить их в кучу обычно более безопасная идея.

Если вам действительно нужно это C, вы можете использовать VLA (нет vla в C ++, тоже неплохо). Они намного лучше, чем alloca () в отношении поведения и согласованности области видимости. Как я вижу, VLA - это своего рода правильно выполненная функция alloca () .

Конечно, локальная структура или массив, использующий мажоранту необходимого пространства, все еще лучше, и если у вас нет такого мажорантного выделения кучи с использованием обычного malloc (), вероятно, это нормально. Я не вижу ни одного разумного варианта использования, когда вам действительно нужен ни alloca (), ни VLA.


Я не вижу причины понижения (кстати, без каких-либо комментариев)
gd1

Только имена имеют область. allocaне создает имя, только диапазон памяти, который имеет время жизни .
любопытный парень

@curiousguy: ты просто играешь словами. Для автоматических переменных я мог бы также говорить о времени жизни базовой памяти, поскольку оно соответствует области имени. В любом случае, проблема не в том, как мы это называем, а в нестабильности времени жизни / объема памяти, возвращаемых alloca, и исключительном поведении.
Крис

2
Я хотел бы, чтобы alloca имел соответствующее «freea» со спецификацией, что вызов «freea» отменял бы эффекты «alloca», который создал объект и все последующие, и требованием, чтобы хранилище «alloca» находилось внутри функции. быть «свободным» внутри него. Это позволило бы практически всем реализациям поддерживать alloca / freea совместимым образом, облегчило бы проблемы с встраиванием и, как правило, сделало бы вещи намного чище.
суперкат

2
@supercat - я тоже этого хочу. По этой причине (и более), я использую слой абстракции ( в основном макросы и встроенные функции) , так что я никогда не называю allocaили mallocили freeнепосредственно. Я говорю такие вещи, как {stack|heap}_alloc_{bytes,items,struct,varstruct}и {stack|heap}_dealloc. Так что, heap_deallocпросто звонит freeи stack_deallocне работает. Таким образом, распределение стека может быть легко понижено до распределения кучи, и намерения также более ясны.
Тодд Леман

9

Вот почему:

char x;
char *y=malloc(1);
char *z=alloca(&x-y);
*z = 1;

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

Практически весь код, использующий allocaи / или C99 vlas, имеет серьезные ошибки, которые приведут к сбоям (если вам повезет) или компрометации привилегий (если вам не повезет).


1
Мир может никогда не узнать. :( Тем не менее, я надеюсь, что вы могли бы уточнить вопрос, который у меня есть alloca. Вы сказали, что почти во всем коде, который использует его, есть ошибка, но я планировал ее использовать; я обычно игнорировал бы такое утверждение, но буду от вас я не буду. Я пишу виртуальную машину, и я хотел бы выделить переменные, которые не выходят из функции в стеке, а не динамически, из-за огромного ускорения. Есть ли альтернатива Подход, который имеет те же характеристики производительности? Я знаю, что могу приблизиться к пулам памяти, но это все еще не так дешево. Что бы вы сделали?
GManNickG

7
Знаете, что также опасно? Это: *0 = 9;УДИВИТЕЛЬНО !!! Я думаю, что я никогда не должен использовать указатели (или, по крайней мере, разыменовывать их). Погоди Я могу проверить, чтобы видеть, является ли это нулем. Хм. Я думаю, что я также могу проверить размер памяти, которую я хочу выделить через alloca. Странный человек Weird.
Томас Эдинг

7
*0=9;не является допустимым C. Что касается тестирования размера, который вы передаете alloca, проверить его на что? Нет никакого способа узнать предел, и если вы просто собираетесь протестировать его на крошечном фиксированном известном безопасном размере (например, 8 КБ), вы можете просто использовать массив фиксированного размера в стеке.
R .. GitHub ОСТАНОВИТЬСЯ, ПОМОГАЯ ЛЬДУ

7
Проблема с вашим аргументом «либо размер известен как достаточно маленький, либо зависит от ввода и, следовательно, может быть сколь угодно большим», как я вижу, заключается в том, что он так же сильно применим к рекурсии. Практический компромисс (для обоих случаев) состоит в том, чтобы предположить, что, если размер ограничен, small_constant * log(user_input)то у нас, вероятно, достаточно памяти.
j_random_hacker

1
Действительно, вы определили ОДИН случай, когда VLA / alloca полезен: рекурсивные алгоритмы, где максимальное пространство, необходимое в любом кадре вызова, может быть равно N, но где сумма пространства, необходимого на всех уровнях рекурсии, равна N или некоторой функции н то быстро не растет.
R .. GitHub ОСТАНОВИТЬ ЛЬДА

9

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

Например, общая оптимизация компиляторами C состоит в том, чтобы исключить использование указателя кадра внутри функции, доступ к кадру осуществляется вместо указателя стека; так что есть еще один регистр для общего пользования. Но если в функции вызывается alloca, разница между sp и fp будет неизвестна для части функции, поэтому такая оптимизация не может быть выполнена.

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

ОБНОВЛЕНИЕ: Так как локальные массивы переменной длины были добавлены в C, и поскольку они представляют очень похожие проблемы генерации кода для компилятора, как alloca, я вижу, что «редкость использования и теневое состояние» не применимы к базовому механизму; но я все еще подозреваю, что использование alloca или VLA ведет к компрометации процесса генерации кода внутри функции, которая их использует. Буду рад любым отзывам от разработчиков компиляторов.


1
Массивы переменной длины никогда не добавлялись в C ++.
Нир Фридман

@NirFriedman Действительно. Я думаю, что был список функций Википедии, который был основан на старом предложении.
Грегго

> Я бы до сих пор подозреваю , что использование либо ALLOCA или VLA стремится к генерации кода компромиссом я думаю , что использование ALLOCA требует указатель кадра, поскольку указатель стека перемещается способами , которые не являются очевидными во время компиляции. alloca может вызываться в цикле, чтобы продолжать захват большего количества стековой памяти, или с вычисленным размером во время выполнения и т. д. Если указатель кадра есть, сгенерированный код имеет устойчивую ссылку на локальные объекты, и указатель стека может делать все, что захочет; это не используется.
Каз

8

Одна ловушка с allocaэтим longjmpперематывает это.

То есть, если вы сохраняете контекст setjmp, затем allocaнекоторую память, а затем longjmpв контекст, вы можете потерять allocaпамять. Указатель стека возвращается туда, где он был, и поэтому память больше не резервируется; если вы вызываете функцию или делаете другую alloca, вы закроете оригинал alloca.

Чтобы прояснить, что я конкретно имею в виду здесь, это ситуация, при longjmpкоторой не возвращается из функции, в которой allocaпроизошла ошибка! Скорее функция сохраняет контекст с setjmp; затем выделяет память с помощью allocaи, наконец, longjmp имеет место для этого контекста. allocaПамять этой функции не полностью освобождена; просто вся память, которую он выделил со времен setjmp. Конечно, я говорю о наблюдаемом поведении; ни одно из таких требований не задокументировано ни одним из известных allocaмне.

В документации основное внимание уделяется концепции, что allocaпамять связана с активацией функции , а не с каким-либо блоком; что множественные вызовы allocaпросто захватывают больше стековой памяти, которая освобождается после завершения функции. Не так; память фактически связана с контекстом процедуры. Когда контекст восстанавливается с помощью longjmp, также происходит и предыдущее allocaсостояние. Это является следствием того, что сам регистр указателя стека используется для выделения, а также (обязательно) сохраняется и восстанавливается в jmp_buf.

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

Я столкнулся с этим как основной причиной ошибки.


1
Это то, что он должен делать - longjmpвозвращается и делает так, что программа забывает обо всем, что происходило в стеке: все переменные, вызовы функций и т. Д. И allocaточно так же, как массив в стеке, поэтому ожидается, что они будут забиты как и все остальное в стеке.
tehftw

1
man allocaдал следующее предложение: «Поскольку пространство, выделенное alloca (), выделяется внутри фрейма стека, это пространство автоматически освобождается, если возврат функции перепрыгивается при вызове longjmp (3) или siglongjmp (3).». Таким образом, задокументировано, что память, выделенная с, allocaстановится забитой после a longjmp.
tehftw

@tehftw Описанная ситуация возникает без перехода через функцию return longjmp. Целевая функция еще не вернулась. Это было сделано setjmp, allocaа затем longjmp. longjmpМожет перемотать allocaсостояние обратно к тому , что это было на setjmpвремя. То есть перемещаемый указатель allocaстрадает от той же проблемы, что и локальная переменная, которая не была помечена volatile!
Каз

3
Я не понимаю, почему это должно быть неожиданным. Когда вы setjmpтогда alloca, и тогда longjmp, это нормально, что allocaбудет вознаграждено. Весь смысл в longjmpтом, чтобы вернуться в состояние, в котором был сохранен setjmp!
августа

@tehftw Я никогда не видел, чтобы это конкретное взаимодействие было задокументировано. Следовательно, нельзя полагаться ни на один из способов, кроме как на эмпирическое исследование с составителями.
Каз

7

Место, где alloca()особенно опасно, чем malloc()ядро - ядро ​​типичной операционной системы имеет пространство стека фиксированного размера, жестко запрограммированное в одном из его заголовков; это не так гибко, как стек приложения. Выполнение вызова alloca()с необоснованным размером может привести к сбою ядра. Некоторые компиляторы предупреждают об использовании alloca()(и даже VLA в этом отношении) при определенных параметрах, которые должны быть включены при компиляции кода ядра - здесь лучше выделять память в куче, которая не зафиксирована жестко заданным пределом.


7
alloca()не более опасен, чем int foo[bar];где- barто произвольное целое число.
Тодд Леман

@ToddLehman Это верно, и именно по этой причине мы запретили VLA в ядре в течение нескольких лет, и с 2018 года свободны от VLA :-)
Chris Down

6

Если вы случайно записываете за пределы блока, выделенного с помощью alloca(например, из-за переполнения буфера), вы перезапишете адрес возврата вашей функции, потому что он расположен «выше» в стеке, то есть после выделенного блока.

Блок _alloca в стеке

Последствия этого двояки:

  1. Программа аварийно завершит работу, и невозможно будет сказать, почему или где произошел сбой (стек, скорее всего, будет разматываться на случайный адрес из-за перезаписанного указателя кадра).

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

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


11
Ничто в этой ситуации кардинально не отличается от опасностей переполнения буфера в буфере фиксированного размера, выделенном стеком. Эта опасность не уникальна для alloca.
телефонный маркер

2
Конечно, нет. Но, пожалуйста, проверьте оригинальный вопрос. Вопрос в том, чем это опасно по allocaсравнению с malloc(таким образом, буфер не фиксированного размера в стеке).
rustyx

Минорная точка, но стеки в некоторых системах растут вверх (например, 16-разрядные микропроцессоры PIC).
EBlake

5

К сожалению, по-настоящему потрясающее alloca()отсутствует в почти потрясающем TCC. GCC имеет alloca().

  1. Он сеет семена своего собственного уничтожения. С возвращением в качестве деструктора.

  2. Как будто malloc()он возвращает неверный указатель при сбое, что приведет к сбою в современных системах с MMU (и, надеюсь, перезапустит те, у кого нет).

  3. В отличие от автоматических переменных вы можете указать размер во время выполнения.

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

Если вы нажмете слишком глубоко, вы уверены в сегменте (если у вас есть MMU).

Обратите внимание, что он malloc()больше не предлагает, так как возвращает NULL (который также будет иметь значение segfault, если назначено), когда системе не хватает памяти. Т.е. все, что вы можете сделать, это внести залог или просто попытаться назначить его любым способом.

Для использования malloc()я использую глобалы и присваиваю им NULL. Если указатель не NULL, я освобождаю его перед использованием malloc().

Вы также можете использовать в realloc()качестве общего случая, если хотите скопировать любые существующие данные. Вы должны проверить указатель, прежде чем выяснить, собираетесь ли вы копировать или объединять после realloc().

3.2.5.2 Преимущества alloca


4
На самом деле спецификация alloca не говорит, что возвращает неверный указатель при сбое (переполнение стека), она говорит, что имеет неопределенное поведение ... и для malloc она говорит, что возвращает NULL, а не случайный неверный указатель (ОК, оптимистичная реализация памяти в Linux делает это бесполезный).
Крис

@kriss Linux может убить ваш процесс, но, по крайней мере, он не
решится

@ craig65535: выражение « неопределенное поведение» обычно означает, что такое поведение не определено спецификациями C или C ++. Ни в коем случае это не будет случайным или нестабильным на любой ОС или компиляторе. Поэтому бессмысленно ассоциировать UB с именем ОС, такой как «Linux» или «Windows». Это не имеет к этому никакого отношения.
Kriss

Я пытался сказать, что malloc, возвращающий NULL, или, в случае Linux, доступ к памяти, убивающий ваш процесс, предпочтительнее неопределенного поведения alloca. Я думаю, что я неправильно прочитал ваш первый комментарий.
craig65535

3

У процессов только ограниченный объем доступного стекового пространства - намного меньше, чем объем доступной памяти malloc().

Используя alloca()вас, вы значительно увеличите свои шансы получить ошибку переполнения стека (если вам повезет, или необъяснимый сбой, если нет).


Это очень сильно зависит от приложения. Для встроенного приложения с ограниченной памятью нередко размер стека превышает размер кучи (если даже есть куча).
EBlake

3

Не очень красиво, но если производительность действительно имеет значение, вы можете заранее выделить место в стеке.

Если вы уже установили максимальный размер блока памяти, который вам нужен, и хотите сохранить проверки переполнения, вы можете сделать что-то вроде:

void f()
{
    char array_on_stack[ MAX_BYTES_TO_ALLOCATE ];
    SomeType *p = (SomeType *)array;

    (...)
}

12
Гарантируется ли правильное выравнивание массива char для любого типа данных? Alloca дает такое обещание.
Юхо Остман

@ JuhoÖstman: вы можете использовать массив struct (или любого другого типа) вместо char, если у вас есть проблемы с выравниванием.
Крис


3

Функция alloca великолепна, и все скептики просто распространяют FUD.

void foo()
{
    int x = 50000; 
    char array[x];
    char *parray = (char *)alloca(x);
}

Массив и парринг точно такие же, как и те же риски. Сказать, что один лучше другого - это синтаксический выбор, а не технический.

Что касается выбора переменных стека по сравнению с переменными кучи, есть много преимуществ для долгосрочных программ, использующих стек поверх кучи для переменных с продолжительностью жизни в области. Вы избегаете фрагментации кучи и можете избежать увеличения пространства процесса за счет использования неиспользуемого (непригодного) пространства кучи. Вам не нужно убирать это. Вы можете контролировать выделение стека в процессе.

Почему это плохо?


3

На самом деле, alloca не гарантирует использование стека. Действительно, реализация alloca в gcc-2.95 выделяет память из кучи, используя сам malloc. Кроме того, эта реализация содержит ошибки, это может привести к утечке памяти и неожиданному поведению, если вы вызовете ее внутри блока с дальнейшим использованием goto. Нет, чтобы сказать, что вы никогда не должны использовать его, но иногда иногда alloca приводит к большим накладным расходам, чем освобождает от фрома.


Похоже, что gcc-2.95 сломал alloca и, вероятно, не может быть безопасно использован для программ, которые требуют alloca. Как бы это очистило память, когда longjmpиспользуется, чтобы отказаться от кадров, которые сделали alloca? Когда кто-нибудь сегодня использует gcc 2.95?
Каз

2

ИМХО, alloca считается плохой практикой, потому что все боятся исчерпать ограничение размера стека.

Я многому научился, читая эту ветку и некоторые другие ссылки:

Я использую alloca в основном для того, чтобы мои простые C-файлы можно было компилировать в msvc и gcc без каких-либо изменений, стиль C89, отсутствие #ifdef _MSC_VER и т. Д.

Спасибо ! Эта тема заставила меня зарегистрироваться на этом сайте :)


Имейте в виду, что на этом сайте нет такого понятия, как «ветка». Переполнение стека имеет формат вопросов и ответов, а не формат цепочки обсуждений. «Ответить» не похоже на «Ответить» в дискуссионном форуме; это означает, что вы фактически даете ответ на вопрос и не должны использоваться для ответа на другие ответы или для комментариев по теме. Если у вас есть хотя бы 50 повторений, вы можете оставлять комментарии , но обязательно прочитайте «Когда я не должен комментировать?» раздел. Пожалуйста, прочитайте страницу « О нас», чтобы лучше понять формат сайта.
Ади Инбар

1

На мой взгляд, alloca (), если он доступен, должен использоваться только ограниченным образом. Очень похоже на использование «goto», довольно большое количество разумных в остальном людей сильно отвращается не только к использованию alloca (), но и к его существованию.

Для встроенного использования, где размер стека известен, и ограничения могут быть наложены с помощью соглашения и анализа размера выделения, и где компилятор не может быть обновлен для поддержки C99 +, использование alloca () хорошо, и я был Известно, что использовать его.

При наличии VLA могут иметь некоторые преимущества по сравнению с alloca (): компилятор может генерировать проверки ограничения стека, которые будут перехватывать доступ за пределами границ при использовании доступа в стиле массива (я не знаю, делают ли это какие-либо компиляторы, но это может ), и анализ кода может определить, правильно ли ограничены выражения доступа к массиву. Обратите внимание, что в некоторых средах программирования, таких как автомобильное, медицинское оборудование и авионика, этот анализ должен выполняться даже для массивов фиксированного размера, как автоматического (в стеке), так и статического распределения (глобального или локального).

На архитектурах, которые хранят как данные, так и адреса возврата / указатели фреймов в стеке (из того, что я знаю, это все из них), любая переменная, выделенная в стеке, может быть опасной, потому что адрес переменной может быть взят, и непроверенные входные значения могут позволить все виды вреда.

Переносимость менее важна во встроенном пространстве, однако это хороший аргумент против использования alloca () вне тщательно контролируемых обстоятельств.

За пределами встроенного пространства для эффективности я использовал alloca () в основном внутри функций ведения журнала и форматирования, а также в нерекурсивном лексическом сканере, где временные структуры (выделяемые с помощью alloca () создаются во время токенизации и классификации, а затем - постоянные). объект (выделенный через malloc ()) заполняется до возврата функции. Использование alloca () для меньших временных структур значительно уменьшает фрагментацию при выделении постоянного объекта.


1

Большинство ответов здесь в основном упускают суть: есть причина, почему _alloca() потенциально хуже, чем просто хранение больших объектов в стеке.

Основное отличие между автоматическим хранением _alloca()заключается в том, что последнее страдает от дополнительной (серьезной) проблемы: выделенный блок не контролируется компилятором , поэтому компилятор не может его оптимизировать или переработать.

Для сравнения:

while (condition) {
    char buffer[0x100]; // Chill.
    /* ... */
}

с:

while (condition) {
    char* buffer = _alloca(0x100); // Bad!
    /* ... */
}

Проблема с последним должна быть очевидной.


Есть ли у вас практические примеры, демонстрирующие разницу между VLA и alloca(да, я действительно говорю VLA, потому что allocaэто больше, чем просто создатель статических массивов)?
Руслан

Есть варианты использования для второго, который первый не поддерживает. Я могу захотеть иметь n записей после того, как цикл будет выполнен n раз - возможно, в связанном списке или дереве; эта структура данных затем удаляется, когда функция в конце концов возвращается. Это не значит, что я бы что-нибудь кодировал таким образом :-)
greggo

1
И я бы сказал, что «компилятор не может это контролировать» потому, что именно так определена alloca (); современные компиляторы знают, что такое alloca, и обращаются с ним особенно; это не просто библиотечная функция, как в 80-х. VLA C99 в основном выделены с областью блока (и лучшей типизацией). Не более или менее контроль, просто соответствующий другой семантике.
Грегго

@greggo: Если ты даунвотер, я бы с удовольствием услышал, почему ты считаешь мой ответ бесполезным.
alecov

В C переработка не является задачей компилятора, а является задачей библиотеки c (free ()). alloca () освобождается по возвращении.
Петер - Восстановить Монику

1

Я не думаю, что кто-то упоминал об этом, но у alloca также есть некоторые серьезные проблемы с безопасностью, которые не обязательно присутствуют в malloc (хотя эти проблемы также возникают с любыми массивами на основе стека, динамическими или нет). Поскольку память выделяется в стеке, переполнение / переполнение буфера имеет гораздо более серьезные последствия, чем просто malloc.

В частности, адрес возврата для функции хранится в стеке. Если это значение будет повреждено, ваш код может быть перемещен в любую исполняемую область памяти. Компиляторы делают все возможное, чтобы сделать это трудным (в частности, путем рандомизации размещения адресов). Тем не менее, это явно хуже, чем просто переполнение стека, так как лучший случай - SEGFAULT, если возвращаемое значение повреждено, но он также может начать выполнять случайный кусок памяти или в худшем случае некоторую область памяти, которая ставит под угрозу безопасность вашей программы. ,

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.