Чому використання аллока () не вважається хорошою практикою?


400

alloca()виділяє пам'ять на стек, а не на купу, як у випадку malloc(). Отже, коли я повертаюся з розпорядку, пам'ять звільняється. Отже, насправді це вирішує мою проблему звільнення динамічно розподіленої пам'яті. Звільнення пам'яті, що виділяється, malloc()є головним болем, і якщо якимось чином пропущене, призводить до всіляких проблем з пам'яттю.

Чому використання alloca()знешкоджених незважаючи на вищезазначені особливості?


40
Просто швидка примітка. Хоча ця функція може бути знайдена в більшості компіляторів, вона не вимагається стандартом ANSI-C і, отже, може обмежувати переносимість. Інша справа, що ти не повинен! free () вказівник, який ви отримуєте, і він автоматично звільняється після виходу з функції.
merkuro

9
Крім того, функція з alloca () не буде вкладена, якщо оголошена такою.
Justicle

2
@Justicle, ви можете дати пояснення? Мені дуже цікаво, що стоїть за такою поведінкою
migajek

47
Забудьте про всі шуми щодо переносимості, не потрібно дзвонити free(що, очевидно, є перевагою), неможливість вбудовувати його (очевидно, що купові виділення набагато важчі) тощо. Єдина причина цього уникати alloca- великі розміри. Тобто витрачати тонни пам'яті стека - це не дуже гарна ідея, плюс у вас є шанс переповнення стека. У такому випадку - подумайте про використання malloca/freea
valdo

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

Відповіді:


245

Відповідь знаходиться прямо на manсторінці (принаймні в Linux ):

ПОВЕРНЕННЯ ЗНАЧЕННЯ Функція alloca () повертає покажчик на початок виділеного простору. Якщо виділення викликає переповнення стека, поведінка програми не визначена.

Що не означає, що його ніколи не слід використовувати. Один із проектів OSS, над яким я працюю, широко використовує його, і поки ви не зловживаєте ним ( alloca'величезні значення'), це добре. Після того, як ви пройдете повз позначку "кілька сотень байтів", настав час mallocнатомість використовувати та друзів. Ви все ще можете отримати збої в розподілі, але принаймні ви будете мати певні вказівки на помилку, а не просто видувати стек.


35
Тож насправді з цим немає жодної проблеми, яку б у вас не було і при оголошенні великих масивів?
ТЕД,

88
@Sean: Так, ризик переповнення стека - це причина, але ця причина трохи нерозумна. По-перше, тому, що (як каже Вайбхав) великі локальні масиви викликають точно таку ж проблему, але вони не є настільки злісною. Також рекурсія може так само легко підірвати стек. Вибачте, але я надію вас на те, щоб сподіватись на поширену думку про те, що причина, наведена на сторінці чоловіка, виправдана.
j_random_hacker

49
Моя думка полягає в тому, що виправдання, наведене на сторінці man, не має сенсу, оскільки alloca () точно такий же «поганий», як і інші речі (локальні масиви або рекурсивні функції), які вважаються кошерними.
j_random_hacker

39
@ninjalj: Не досвідчені програмісти C / C ++, але я думаю, що багато людей, які побоюються 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
Цікаво. Але хіба це не кваліфікується як помилка компілятора? Зрештою, вбудовування змінило поведінку коду (це затримало звільнення простору, виділеного за допомогою аллока).
sleske

60
Мабуть, щонайменше GCC врахує це: "Зауважте, що певні звичаї у визначенні функції можуть зробити його непридатним для заміни вбудованими. Серед цих звичаїв: використання varargs, використання aloca, [...]". gcc.gnu.org/onlinedocs/gcc/Inline.html
sleske

136
Який компілятор ви курили?
Томас Едінг

22
Що я не розумію, це те, чому компілятор не використовує сферу застосування, щоб визначити, що аллокас у підскопі є більш-менш "звільненим": покажчик стека може повернутися до своєї точки, перш ніж вводити область, як, що робиться, коли повернення з функції (не міг?)
moala

7
Я спростував, але відповідь добре написана: я погоджуюся з іншими, хто винить аллоку за те, що явно помилка компілятора . Компілятор зробив помилкове припущення в оптимізації, якого він не повинен був робити. Обійти з помилкою компілятора - це добре, але я не став би нічого винного за це, крім компілятора.
Еван Керролл

75

Старе питання, але ніхто не згадував, що його слід замінити масивами змінної довжини.

char arr[size];

замість

char *arr=alloca(size);

Він знаходиться в стандартному С99 і існував як розширення компілятора у багатьох компіляторах.


5
Про це згадує Джонатан Леффлер у коментарі до відповіді Артура Ульфельда.
ніндзя

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

6
Одна примітка - це масиви змінної довжини, а не динамічні масиви. Останні змінює розмір і зазвичай реалізується на купі.
Тім Час

1
Visual Studio 2015, що компілює деякі C ++, має той самий випуск.
ahcox

2
Лінус Торвальдс не любить VLA в ядрі Linux . Станом на версію 4.20 Linux має бути майже без VLA.
Крістіан

60

alloca () дуже корисний, якщо ви не можете використовувати стандартну локальну змінну, оскільки її розмір потрібно визначити під час виконання, і ви можете абсолютно гарантувати, що покажчик, який ви отримаєте від alloca (), НІКОЛИ не буде використаний після повернення цієї функції .

Ти можеш бути досить безпечним, якщо ти

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

Справжня небезпека випливає з того, що десь пізніше хтось порушить ці умови. Зважаючи на це, це чудово для передачі буферів до функцій, які форматують текст у них :)


12
Функція VLA (масив змінної довжини) C99 підтримує локальні змінні з динамічним розміром, не вимагаючи явного використання аллока ().
Джонатан Леффлер

2
неато! знайшов додаткову інформацію в розділі "3.4 Масив змінної довжини" програми
programersheaven.com/2/Pointers-and-Arrays-page-2

1
Але це не відрізняється від обробки покажчиками до локальних змінних. Їх також можна обдурити ...
glglgl

2
@Jonathan Leffler одна річ, яку ти можеш зробити з аллока, але ти не можеш робити з VLA, використовує ключове слово обмеження. Ось так: float * обмежувати тяжко_використовувати_arr = alloca (sizeof (float) * size); замість плавати важко_використано_арр [розмір]. Це може допомогти деяким компіляторам (у моєму випадку gcc 4.8) оптимізувати збірку, навіть якщо розмір є постійною компіляцією. Дивіться моє запитання про це: stackoverflow.com/questions/19026643/using-restrict-with-arrays
Петро Lopusiewicz

@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
Здається, пов'язаний пост написаний Джоном Левайн - чувак, який написав "Вантажники та навантажувачі", я б довіряв тому, що він сказав.
користувач318904

3
Пов'язаний пост - це відповідь на публікацію Джона Левіна.
А. Вількокс

6
Майте на увазі, багато що змінилося з 1991 року. Усі сучасні компілятори C (навіть у 2009 році) повинні обробляти аллока як особливий випадок; це не властива, а звичайна функція, і навіть не може викликати функцію. Отже, питання про параметри алокації (який виник у K&R C з 1970-х років) не повинен бути проблемою зараз. Більш детально в коментарі я зробив відповідь Тоні Д
greggo

26

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


21

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

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

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

    У багатьох системах аллока () не може бути використана всередині списку аргументів виклику функції, оскільки простір стека, зарезервований аллока (), з'явиться на стеці посеред простору для аргументів функції.


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

#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

Коментар з сторінки linux дуже старий і, я впевнений, застарів. Усі сучасні компілятори знають, що таке аллока (), і не подорожуватимуть за своїми шнурками. У старому K&R C (1) усі функції, що використовуються покажчиками кадрів (2) Усі виклики функцій були {push args on stack} {call func} {add # n, sp}. Аллока - це функція lib, яка просто збільшила стек, компілятор навіть не знав про те, що відбувається. (1) і (2) вже не відповідають дійсності, тому аллока не може працювати таким чином (тепер це невід'ємна). У старій мові виклик алоки посеред штовхаючих аргументів очевидно також порушив би ці припущення.
greggo

4
Щодо прикладу, я б взагалі стурбований чимось, що вимагає always_inline, щоб уникнути пошкодження пам'яті ....
greggo

14

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

Іншими словами, alloca( 0x00ffffff )це небезпечно і може спричинити переповнення рівно стільки, скільки char hugeArray[ 0x00ffffff ];є. Будьте обережні та розумні, і у вас все буде добре.


12

Дуже багато цікавих відповідей на це "старе" питання, навіть деякі відносно нові відповіді, але я не знайшов жодної, яка б це згадувала ....

При правильному та обережному застосуванні послідовне використання alloca() (можливо, на всьому застосуванні) для обробки невеликих розподілів із змінною довжиною (або C99 VLAs, якщо вони доступні) може призвести до зниження загального зростання стека, ніж інакше еквівалентна реалізація з використанням великих розмірів локальних масивів фіксованої довжини . Тож alloca()може бути корисним для вашої стеки, якщо ви ретельно використовуєте її.

Я знайшов цю цитату в .... Добре, я склав цю цитату. Але справді, подумайте про це….

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

Я працював на робочому столі / сервері та вбудованих системах. Багато вбудованих систем взагалі не використовують купу (вони навіть не посилаються на її підтримку) з причин, які включають уявлення про те, що динамічно виділена пам'ять є злою через ризики витоку пам'яті у програмі, яка ніколи коли-небудь перезавантажуватися роками за один раз, або більш розумне обгрунтування того, що динамічна пам’ять небезпечна, оскільки не може бути точно відомо, що програма ніколи не буде фрагментувати свою купу до точки помилкового вичерпання пам'яті. Тому вбудованим програмістам залишається мало альтернатив.

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

Я знову бачив час і час, коли програміст робить буфер, виділений стеком, "достатньо великий, щоб обробляти будь-який можливий випадок". У глибоко вкладеному дереві викликів багаторазове використання цього (анти -?) Шаблону призводить до перебільшеного використання стека. (Уявіть дерево викликів глибиною 20 рівнів, де на кожному рівні з різних причин функція сліпо перерозподіляє буфер у 1024 байти "просто для безпечності", коли загалом він буде використовувати лише 16 або менше, і лише в дуже рідкісні випадки можуть використовувати більше.) Альтернатива - використанняalloca()або VLA та виділити лише стільки простору стеку, скільки потрібно вашій функції, щоб уникнути зайвого навантаження на стек. Будемо сподіватися, що коли одна функція в дереві викликів потребує розподілу більше, ніж зазвичай, інші в дереві викликів все ще використовують свої звичайні невеликі асигнування, і загальне використання стека додатків значно менше, ніж якщо кожна функція сліпо перерозподіляє локальний буфер .

Але якщо ви вирішите використовувати alloca()...

Виходячи з інших відповідей на цій сторінці, здається, що VLA повинні бути безпечними (вони не з'єднують виділення стеків, якщо викликаються з циклу), але якщо ви використовуєте alloca(), будьте обережні, щоб не використовувати їх у циклі, і зробіть переконайтеся, що ваша функція не може бути вбудована, якщо є ймовірність, що вона може бути викликана в циклі іншої функції.


Я згоден з цим пунктом. Небезпека alloca()правдива, але те ж саме можна сказати і про витоки пам'яті malloc()(чому б тоді не використовувати GC? Можна стверджувати). alloca()при обережному використанні може бути дуже корисним для зменшення розміру стека.
Феліпе Тонелло

Ще одна вагома причина - не використовувати динамічну пам'ять, особливо у вбудованій: це складніше, ніж дотримуватися стека. Використання динамічної пам’яті вимагає спеціальних процедур та структур даних, тоді як на стеці справа (для спрощення речей) є питанням додавання / віднімання більшого числа з стекпоінтера.
tehftw

Sidenote: Приклад "за допомогою фіксованого буфера [MAX_SIZE]" підкреслює, чому так добре працює політика перезапису пам'яті. Програми виділяють пам'ять, яку вони ніколи не можуть торкатися, за винятком меж їх довжини буфера. Тож прекрасно, що Linux (та інші ОС) насправді не призначають сторінку пам'яті до її першої використання (на відміну від malloc'd). Якщо буфер перевищує одну сторінку, програма може використовувати лише першу сторінку, а не витрачати решту фізичної пам'яті.
Катастичне подорож

@KatasticVoyage Якщо розмір сторінки віртуальної пам’яті вашої системи не перевищує (або принаймні дорівнює) розміру сторінки віртуальної пам’яті вашої системи, ваш аргумент не заперечує. Також у вбудованих системах без віртуальної пам’яті (у багатьох вбудованих MCU немає MMU) політика пам’яті надвикористання може бути корисною з точки зору „забезпечте, щоб ваша програма працювала у будь-яких ситуаціях”, але ця гарантія залежить від ціни на розмір вашої стеки повинні також бути виділені для підтримки цієї політики переповнення пам'яті. У деяких вбудованих системах це ціна, яку деякі виробники недорогих товарів не бажають платити.
зворотній зв'язок

11

Усі вже вказали на велику річ, яка є потенційною невизначеною поведінкою через переповнення стека, але я мушу зазначити, що середовище Windows має чудовий механізм, щоб зрозуміти це за допомогою структурованих винятків (SEH) та сторожових сторінок. Оскільки стек росте лише у міру необхідності, ці охоронні сторінки розміщуються в місцях, які не розміщені. Якщо ви виділите в них (переповнюючи стек), викидається виняток.

Ви можете зловити цей виняток SEH і зателефонувати _resetstkoflw, щоб скинути стек і продовжити ваш веселий шлях. Це не ідеально, але це ще один механізм, принаймні знати, що щось пішло не так, коли штука потрапляє у вентилятор. * nix може мати щось подібне, про що я не знаю.

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

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


Проблема полягає в тому, що alloca()можна зажадати стільки місця, що точковий покажчик приземлиться в купу. З цим нападник, який може контролювати розмір alloca()та дані, які надходять у цей буфер, може перезаписати купу (що дуже погано).
12431234123412341234123

SEH - річ лише для Windows. Це чудово, якщо ви дбаєте лише про те, щоб ваш код працював у Windows, але якщо ваш код повинен бути кросплатформенним (або якщо ви пишете код, який працює лише на платформі, яка не є Windows), ви не можете розраховувати на наявність SEH.
Джордж

10

Аллока () приємний і ефективний ... але він також глибоко порушений.

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

У більшості випадків ви можете замінити його за допомогою локальних змінних та розміру мажоранта. Якщо його використовують для великих предметів, їх розміщення, як правило, є більш безпечною ідеєю.

Якщо він вам справді потрібен, ви можете використовувати VLA (немає vla в C ++, теж погано). Вони значно кращі, ніж аллока () щодо поведінки та послідовності сфери. Як я бачу, VLA - це свого роду аллока (), зроблений правильно.

Звичайно, локальна структура або масив, що використовує мажорант потрібного простору, все ще краще, і якщо у вас немає такого розміщення мажорантової купи за допомогою простого malloc (), ймовірно, розумним. Я не бачу жодного розумного випадку використання, де вам дійсно потрібні алока () або VLA.


Я не бачу причин для пониження (до речі, без коментарів)
gd1,

Тільки імена мають сферу застосування. allocaне створює ім'я, а лише діапазон пам'яті, який має весь термін експлуатації .
curiousguy

@curiousguy: ти просто граєш зі словами. Для автоматичних змінних я міг би також говорити про термін експлуатації базової пам'яті, оскільки він відповідає обсягу назви. У всякому разі, проблема полягає не в тому, як ми це називаємо, а в нестабільності життя / обсягу пам’яті, що повертається аллока, та винятковій поведінці.
kriss

2
Я хотів би, щоб аллока мав відповідну "фрейду", із зазначенням, що виклик "фріа" скасує ефекти "алока", який створив об'єкт, і всі наступні, і вимога, щоб сховище "алока" входило в fucntion бути "звільненим" і в ньому. Це дало б можливість майже всім реалізаціям підтримувати аллока / фрейду сумісним способом, полегшило б проблеми, що склалися, і, як правило, зробила речі набагато чистішими.
supercat

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, має серйозні помилки, що призведе до збоїв (якщо вам пощастить) або компромісних привілеїв (якщо вам не так пощастить).


1
Світ може ніколи не знати. :( Це сказало, я сподіваюся, що ви зможете прояснити питання, про яке у мене є alloca. Ви сказали, що майже у всіх кодах, які його використовують, є помилка, але я планував її використовувати; я зазвичай ігнорую таку заяву, але приходжу Я пишу віртуальну машину, і я хотів би виділити змінні, які не виходять із функції на стеці, а не динамічно, через величезну швидкість. Чи є альтернативна підхід, який має однакові характеристики продуктивності? Я знаю, що я можу наблизитися до пулів пам'яті, але це все ще не так дешево. Що б ви зробили?
GManNickG

7
Знаєте, що теж небезпечно? Це: *0 = 9;ДУМОВИЙ !!! Я думаю, я ніколи не повинен використовувати покажчики (або, принаймні, відкидати їх). Помилка, зачекайте. Я можу перевірити, чи є він недійсним. Хм. Я думаю, що я також можу перевірити розмір пам'яті, яку я хочу виділити через alloca. Дивна людина. Дивно.
Томас Едінг

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

7
Проблема з вашим "або розмір, як відомо, є досить малим, або він залежить від вводу, і тому може бути довільно великим" аргументом, оскільки я бачу, що він застосовується так само сильно до рекурсії. Практичним компромісом (для обох випадків) є припущення, що якщо розмір обмежений, small_constant * log(user_input)тоді, ймовірно, нам вистачить пам'яті.
j_random_hacker

1
Дійсно, ви визначили ОДНІЙ випадок, коли VLA / alloca є корисним: рекурсивні алгоритми, де максимальний простір, необхідний для будь-якого кадру виклику, може бути таким же, як N, але де сума простору, необхідного на всіх рівнях рекурсії, становить N або деяку функцію з N, що не росте швидко.
R .. GitHub ЗАСТАНОВИТИ ДІЙ

9

Я не думаю, що хтось це згадував: Використання аллока у функції перешкоджатиме або відключає деякі оптимізації, які в іншому випадку можуть бути застосовані до функції, оскільки компілятор не може знати розмір кадру стека функції.

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

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

ОНОВЛЕННЯ: Оскільки локальні масиви змінної довжини додані до C, і оскільки вони представляють дуже схожі проблеми генерації коду для компілятора, як аллока, я бачу, що "рідкість використання та тінистий статус" не поширюється на базовий механізм; але я все ще підозрюю, що використання або alloca, або VLA має тенденцію до компрометації генерації коду в межах функції, яка їх використовує. Я вітаю будь-які відгуки дизайнерів-компіляторів.


1
Масиви змінної довжини ніколи не додавалися до C ++.
Нір Фрідман

@NirFriedman Дійсно. Я думаю, був список функцій вікіпедії, який базувався на старій пропозиції.
greggo

> Я все ще підозрюю, що використання або alloca, або VLA має тенденцію до компрометації генерації коду, я б вважав, що для використання аллока потрібен покажчик кадру, тому що покажчик стека рухається способами, не очевидними під час компіляції. Аллока може бути викликана в циклі, щоб зберегти більше пам'яті стека, або з розрахунковим розміром часу виконання тощо. Якщо є покажчик кадру, згенерований код має стійке посилання на місцевих жителів і покажчик стека може робити все, що завгодно; він не використовується.
Каз

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дав таке речення: "Оскільки простір, виділений аллока (), виділяється в рамці стека, цей простір автоматично звільняється, якщо повернення функції переходить через виклик longjmp (3) або siglongjmp (3)." Отже, задокументовано, що пам'ять, виділена з, allocaстає халобером після a longjmp.
tehftw

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

3
Я не розумію, чому це мало бути несподіваним. Коли setjmpто allocaі тоді longjmp, то це нормально, що allocaбуло б перемотане назад. Вся справа в longjmpтому, щоб повернутися до стану, який було врятовано setjmp!
tehftw

@tehftw Я ніколи не бачив цієї конкретної взаємодії документально підтвердженою. Отже, на неї не можна покластися жодним чином, окрім емпіричного дослідження з укладачами.
Каз

7

Місце, де alloca()особливо небезпечно, ніж malloc()ядро - ядро ​​типової операційної системи має фіксований розмір простору стека, жорстко закодований в один із його заголовків; він не такий гнучкий, як стек програми. Здійснення дзвінка alloca()з необґрунтованим розміром може призвести до збою ядра. Деякі компілятори попереджають про використання alloca()(і навіть VLA для цього питання) під певними параметрами, які слід увімкнути під час компіляції коду ядра - тут краще виділити пам'ять у купі, яка не фіксована жорстко закодованим лімітом.


7
alloca()не є більш небезпечним, ніж int foo[bar];де barє якесь довільне ціле число.
Тодд Леман

@ToddLehman Це правильно, і саме тому ми заборонили VLA в ядрі протягом декількох років, і ми не мали VLA з 2018 року :-)
Chris Down

6

Якщо ви випадково allocaзаписуєте за межі блоку, виділеного з (наприклад, через переповнення буфера), тоді ви перезапишете зворотну адресу своєї функції, тому що ця розташована "вище" на стеці, тобто після виділеного вами блоку.

_alloca блок на стеці

Наслідок цього двоякий:

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

  2. Це робить переповнення буфера в багато разів небезпечнішим, оскільки зловмисник може створити спеціальну корисну навантаження, яка буде розміщена на стеку і тому може закінчитися виконанням.

На противагу цьому, якщо ви пишете поза блоком на купі, ви "просто" отримуєте пошкодження купи. Програма, ймовірно, завершиться несподівано, але належним чином розмотає стек, тим самим знижуючи ймовірність виконання зловмисного коду.


11
Ніщо в цій ситуації кардинально не відрізняється від небезпеки переповнення буфера фіксованого розміру буфера, виділеного стеком. Ця небезпека не властива тільки alloca.
телефонний телефон

2
Звичайно, ні. Але будь ласка, перевірте оригінальне запитання. Питання в тому, яка небезпека allocaв порівнянні з malloc(таким чином, не буфер фіксованого розміру в стеці).
rustyx

Незначна точка, але стеки в деяких системах зростають вгору (наприклад, 16-бітні мікропроцесори PIC).
EBlake

5

На жаль, справді дивовижного alloca()не вистачає майже чудового ТЦК. Gcc є alloca().

  1. Він сіє насіння власного знищення. З поверненням як деструктор.

  2. Начебто malloc()він повертає недійсний покажчик на помилку, який буде розбито на сучасні системи з MMU (і, сподіваємось, перезапустіть без нього).

  3. На відміну від автоматичних змінних, ви можете вказати розмір під час виконання.

Добре справляється з рекурсією. Ви можете використовувати статичні змінні, щоб досягти чогось подібного до хвостової рекурсії та використовувати лише декілька інших для передачі інформації для кожної ітерації.

Якщо ви натискаєте занадто глибоко, ви впевнені в сегменті (якщо у вас є MMU).

Зверніть увагу, що він malloc()не пропонує більше, оскільки він повертає NULL (який також буде сегментарно, якщо він призначений), коли система не має пам'яті. Тобто все, що ви можете зробити - це застава або просто спробувати призначити це будь-яким способом.

Для використання malloc()я використовую глобальні та присвоюю їм NULL. Якщо вказівник не NULL, я звільняю його перед використанням malloc().

Ви також можете використовувати realloc()як загальний регістр, якщо хочете скопіювати будь-які наявні дані. Потрібно перевірити вказівник перед тим, як розібратися, чи збираєтесь ви скопіювати або об'єднати після realloc().

3.2.5.2 Переваги алоки


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

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

@ craig65535: невизначена поведінка виразу зазвичай означає, що ця поведінка не визначена специфікаціями C або C ++. Ні в якому разі, що це буде випадковим або нестабільним для будь-якої ОС або компілятора. Тому асоціювати UB з назвою ОС на зразок "Linux" або "Windows" безглуздо. Це не має нічого спільного.
kriss

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

3

Процеси мають лише обмежену кількість місця в стеці - набагато менше, ніж об'єм пам'яті, доступний malloc().

Використовуючи alloca()ви, різко збільшуйте шанси отримати помилку переповнення стека (якщо вам пощастить, або незрозумілу аварію, якщо ви цього не зробите).


Це дуже залежить від програми. Це незвично для вбудованого додатка з обмеженою пам'яттю розмір стека, більший за купу (якщо є навіть купа).
EBlake

3

Не дуже красиво, але якщо продуктивність дійсно важлива, ви можете виділити трохи місця на стеку.

Якщо ви вже максимум розміру блоку пам'яті, який вам потрібно, і ви хочете продовжувати перевірки на переповнення, ви можете зробити щось на кшталт:

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

    (...)
}

12
Чи гарантовано правильний вирівнювання масиву char для будь-якого типу даних? Аллока надає таку обіцянку.
Juho Östman

@ JuhoÖstman: ви можете використовувати масив struct (або будь-якого типу) замість char, якщо у вас проблеми з відчуженням.
kriss

Це називається масив змінної довжини . Він підтримується в C90 і вище, але не C ++. Див. Чи я можу використовувати масив змінної довжини С у C ++ 03 та C ++ 11?
jww

3

Функція алока чудова, і всі найсеї просто поширюють FUD.

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

Масив та параметри ТОЧНО однакові з РОЗПОВІДАМИ однакових ризиків. Говорити, що один кращий за інший - це синтаксичний вибір, а не технічний.

Що стосується вибору змінних стека проти змінних купи, то існує багато переваг для довгих запущених програм, що використовують стек над купою для змінних із строком життя. Ви уникаєте фрагментації купи і можете уникнути збільшення простору процесу за допомогою невикористаного (непридатного) простору. Не потрібно це прибирати. Ви можете контролювати розподіл стеку в процесі.

Чому це погано?


3

Насправді, аллока не гарантує використання стека. Дійсно, реалізація alc-2,95 аллока виділяє пам'ять із купи за допомогою самого malloc. Крім того, що реалізація є помилковою, вона може призвести до витоку пам'яті та до несподіваної поведінки, якщо ви зателефонуєте всередині блоку з подальшим використанням goto. Не кажучи про те, що ви ніколи не повинні його використовувати, але іноді алока призводить до великих витрат, ніж це звільняє від себе.


Це здається, ніби gcc-2,95 зламав аллоку і, ймовірно, не може бути безпечно використаний для програм, які вимагають alloca. Як би це очистило пам'ять, коли longjmpвона використовується для відмови від кадрів alloca? Коли сьогодні хтось би використовував gcc 2.95?
Каз

2

IMHO, аллока вважається поганою практикою, тому що всі бояться вичерпати обмеження розміру стека.

Я багато чого дізнався, прочитавши цю тему та деякі інші посилання:

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

Дякую ! Ця тема змусила мене зареєструватися на цьому сайті :)


Майте на увазі, що на цьому сайті немає такого поняття, як "нитка". Переповнення стека має формат запитання і відповіді, а не формат теми дискусії. "Відповідь" не схожа на "Відповісти" на дискусійному форумі; це означає, що ви фактично надаєте відповідь на питання, і його не слід використовувати для відповіді на інші відповіді або коментарі до теми. Коли у вас є щонайменше 50 повторень, ви можете залишати коментарі , але обов'язково прочитайте "Коли я не повинен коментувати?" розділ. Будь ласка, прочитайте сторінку About, щоб краще зрозуміти формат сайту.
Аді Інбар

1

На мою думку, аллока (), якщо вона є, повинна використовуватися лише обмежено. Дуже схоже на використання "goto", досить велика кількість інакше розумних людей має сильну неприязнь не лише до використання, але й до існування аллока ().

Для вбудованого використання, коли розмір стека відомий і обмеження можуть бути накладені за допомогою конвенції та аналізу розміру виділення, і де компілятор не може бути оновлений для підтримки C99 +, використання alloca () є нормальним, і я був відомо, що його використовують.

Якщо вони доступні, VLA можуть мати деякі переваги порівняно з alloca (): Компілятор може генерувати перевірку ліміту стеків, яка буде отримувати доступ поза межами діапазону, коли використовується доступ до стилю масиву (я не знаю, чи це роблять якісь компілятори, але це може буде зроблено), і аналіз коду може визначити, чи вирази доступу до масиву правильно обмежені. Зауважте, що в деяких середовищах програмування, таких як автомобільна, медична техніка та авіоніка, цей аналіз потрібно робити навіть для масивів фіксованого розміру, як автоматичного (на стеку), так і статичного розподілу (глобального або локального).

У архітектурах, які зберігають і дані, і зворотні адреси / покажчики кадру на стеці (з того, що я знаю, це все), будь-яка розподілена стека може бути небезпечною, оскільки може бути взята адреса змінної, і неперевірені вхідні значення можуть дозволяти всілякі пустощі.

Переносність викликає занепокоєння у вбудованому просторі, проте це хороший аргумент проти використання аллока () поза ретельно контрольованими обставинами.

Поза вбудованого простору я використовував аллока () здебільшого всередині функцій реєстрації та форматування для ефективності, а також у нерекурсивному лексичному сканері, де тимчасові структури (виділені за допомогою аллока () створюються під час токенізації та класифікації, а потім стійкі Об'єкт (виділений через malloc ()) заповнюється до повернення функції. Використання аллока () для менших тимчасових структур значно зменшує фрагментацію при виділенні стійкого об'єкта.


1

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

Основна відмінність автоматичного зберігання від цього _alloca()полягає в тому, що останній страждає від додаткової (серйозної) проблеми: виділений блок не контролюється компілятором , тому компілятор не може його оптимізувати чи переробити.

Порівняйте:

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

з:

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

Проблема з останнім повинна бути очевидною.


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

Існують випадки використання для другого, який перший не підтримує. Я, можливо, захочуть мати n-записи після того, як цикл буде виконаний 'n' разів - можливо, у пов'язаному списку чи дереві; ця структура даних потім видаляється, коли функція врешті повертається. Що не скажеш, я б так щось
кодував

1
І я б сказав, що "компілятор не може це контролювати", тому що аллока () визначається саме так; сучасні компілятори знають, що таке аллока, і лікують це спеціально; це не просто функція бібліотеки, як це було у 80-х. V99 VLA в основному є розподіленими з областю блоку (і кращим способом набору тексту). Ніякий більш-менш контрольний, просто відповідний різній семантиці.
greggo

@greggo: Якщо ти любитель, я з радістю почую, чому ти вважаєш, що моя відповідь не корисна.
alecov

У C утилізація не є завданням компілятора, натомість це завдання бібліотеки c (free ()). alloca () звільняється при поверненні.
петерх

1

Я не думаю, що хтось про це згадував, але аллока також має серйозні проблеми безпеки, не обов'язково присутніми з malloc (хоча ці проблеми також виникають із будь-якими масивами на основі стека, динамічними чи ні). Оскільки пам’ять виділяється на стек, переповнення / підтоки буфера мають набагато серйозніші наслідки, ніж лише з malloc.

Зокрема, зворотна адреса функції зберігається у стеку. Якщо це значення пошкодиться, ваш код може бути перетворений на будь-яку виконавчу область пам'яті. Компілятори намагаються зробити це важким (зокрема, рандомізувавши макет адреси). Однак це явно гірше, ніж просто переповнення стека, оскільки найкращим випадком є ​​SEGFAULT, якщо повернене значення пошкоджено, але воно може також почати виконувати випадковий фрагмент пам'яті або, в гіршому випадку, якийсь регіон пам'яті, який загрожує безпеці вашої програми. .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.