Dlaczego korzystanie z funkcji splita () nie jest uważane za dobrą praktykę?


400

alloca()przydziela pamięć na stosie, a nie na stercie, jak w przypadku malloc(). Kiedy wracam z rutyny, pamięć zostaje uwolniona. Tak więc faktycznie rozwiązuje to mój problem zwalniania dynamicznie alokowanej pamięci. Zwolnienie pamięci przydzielonej przez malloc()to poważny ból głowy i jeśli w jakiś sposób pominięty, prowadzi do różnego rodzaju problemów z pamięcią.

Dlaczego stosowanie alloca()powyższych funkcji jest odradzane pomimo powyższych funkcji?


40
Krótka notatka. Chociaż tę funkcję można znaleźć w większości kompilatorów, nie jest ona wymagana przez standard ANSI-C i dlatego może ograniczyć przenośność. Inną rzeczą jest to, że nie wolno! free () wskaźnik, który dostajesz i jest on zwalniany automatycznie po wyjściu z funkcji.
merkuro

9
Ponadto funkcja z funkcją splita () nie zostanie wstawiona, jeśli zostanie zadeklarowana jako taka.
Justicle

2
@Justicle, czy możesz podać jakieś wyjaśnienie? Jestem bardzo ciekawy, co kryje się za tym zachowaniem
migajek

47
Zapomnij o hałasie związanym z przenośnością, brakiem potrzeby dzwonienia free(co jest oczywiście zaletą), brakiem możliwości wstawienia go (oczywiście przydzielanie sterty jest znacznie cięższe) itp. Jedynym powodem, dla którego należy unikać, allocasą duże rozmiary. Oznacza to, że marnowanie ton pamięci stosu nie jest dobrym pomysłem, a ponadto istnieje ryzyko przepełnienia stosu. Jeśli tak jest - rozważ użycie malloca/freea
valdo

5
Innym pozytywnym aspektem allocajest to, że stosu nie można podzielić na fragmenty jak stertę. Może to okazać się przydatne w przypadku aplikacji działających w czasie rzeczywistym w trudnych warunkach czasu rzeczywistego, a nawet aplikacji o krytycznym znaczeniu dla bezpieczeństwa, ponieważ WCRU można następnie analizować statycznie bez uciekania się do niestandardowych pul pamięci z własnym zestawem problemów (brak lokalizacji czasowej, nieoptymalne zasoby posługiwać się).
Andreas

Odpowiedzi:


245

Odpowiedź znajduje się na manstronie (przynajmniej w systemie Linux ):

WARTOŚĆ ZWRACANA Funkcja przydzielania () zwraca wskaźnik na początek przydzielonego miejsca. Jeśli przydział powoduje przepełnienie stosu, zachowanie programu jest niezdefiniowane.

Co nie znaczy, że nigdy nie należy go używać. Jeden z projektów OSS, nad którym pracuję, wykorzystuje go w szerokim zakresie i dopóki go nie nadużywasz ( allocaz dużymi wartościami), jest w porządku. Gdy miniesz znak „kilkaset bajtów”, czas na użycie malloci przyjaciół. Nadal możesz otrzymywać awarie alokacji, ale przynajmniej będziesz mieć pewne oznaki niepowodzenia zamiast po prostu wysadzić stos.


35
Więc naprawdę nie ma z tym problemu, którego nie miałbyś również przy deklarowaniu dużych tablic?
TED,

88
@Sean: Tak, podano ryzyko przepełnienia stosu, ale ten powód jest trochę głupi. Po pierwsze dlatego, że (jak mówi Vaibhav) duże tablice lokalne powodują dokładnie ten sam problem, ale nie są tak bardzo oczerniane. Również rekurencja może równie łatwo zniszczyć stos. Przepraszam, ale mam nadzieję, że mam nadzieję przeciwstawić się powszechnemu przekonaniu, że powód podany na stronie podręcznika jest uzasadniony.
j_random_hacker

49
Chodzi mi o to, że uzasadnienie podane na stronie podręcznika nie ma sensu, ponieważ alokacja () jest dokładnie tak „zła”, jak inne rzeczy (tablice lokalne lub funkcje rekurencyjne), które są uważane za koszerne.
j_random_hacker

39
@ninjalj: Nie przez bardzo doświadczonych programistów C / C ++, ale myślę, że wielu ludzi, którzy się boją alloca(), nie ma takiego samego strachu przed lokalnymi tablicami lub rekurencją (w rzeczywistości wiele osób, które będą krzyczeć, alloca()chwalą rekurencję, ponieważ „wygląda elegancko”) . Zgadzam się z radą Shauna („przydział alokacji () jest w porządku w przypadku małych przydziałów”), ale nie zgadzam się z poglądem, że ramy przydziałów () są wyjątkowo złe wśród 3 - są one równie niebezpieczne!
j_random_hacker

35
Uwaga: Biorąc pod uwagę „optymistyczną” strategię alokacji pamięci w systemie Linux, najprawdopodobniej nie dostaniesz żadnych oznak niepowodzenia wyczerpania sterty ... zamiast tego malloc () zwróci ci niezły wskaźnik inny niż NULL, a wtedy, gdy spróbujesz faktycznie dostęp do przestrzeni adresowej, na którą wskazuje, twój proces (lub inny nieprzewidziany proces) zostanie zabity przez zabójcę OOM. Oczywiście jest to „cecha” Linuksa, a nie sama w sobie kwestia C / C ++, ale warto o tym pamiętać, zastanawiając się, czy funkcja calc () czy malloc () jest „bezpieczniejsza”. :)
Jeremy Friesner

209

Jednym z najbardziej pamiętnych błędów, jakie miałem, było użycie wbudowanej funkcji alloca. Przejawiał się jako przepełnienie stosu (ponieważ alokuje na stosie) w losowych punktach wykonania programu.

W pliku nagłówkowym:

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

W pliku implementacyjnym:

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

Tak więc stało się z wbudowaną DoSomethingfunkcją kompilatora, a wszystkie alokacje stosu odbywały się wewnątrz Process()funkcji i tym samym wysadzały stos. W mojej obronie (i to nie ja znalazłem problem; musiałem iść i płakać do jednego ze starszych programistów, kiedy nie mogłem go naprawić), nie było to proste alloca, to była konwersja ciągów ATL makra.

Lekcja brzmi: nie używaj allocaw funkcjach, które Twoim zdaniem mogą być wbudowane.


91
Ciekawy. Ale czy nie kwalifikuje się to jako błąd kompilatora? W końcu wprowadzanie zmieniło zachowanie kodu (opóźniło zwolnienie miejsca przydzielonego za pomocą alokacji).
sleske

60
Najwyraźniej GCC weźmie to pod uwagę: „Zauważ, że niektóre zastosowania w definicji funkcji mogą sprawić, że nie będzie ona odpowiednia do zastępowania w linii. Wśród tych zastosowań są: użycie varargs, użycie divaa, [...]”. gcc.gnu.org/onlinedocs/gcc/Inline.html
sleske

135
Jaki kompilator paliłeś?
Thomas Eding,

22
Nie rozumiem, dlaczego kompilator nie korzysta z zakresu w celu ustalenia, czy alokacje w podskopie są mniej więcej „uwolnione”: wskaźnik stosu może wrócić do swojego punktu przed wejściem w zakres, podobnie jak to dzieje się, gdy powrót z funkcji (prawda?)
moala

7
Zgłosiłem się negatywnie, ale odpowiedź jest dobrze napisana: Zgadzam się z innymi, że popełniacie błąd przydziału dla tego, co jest wyraźnie błędem kompilatora . Kompilator źle podjął optymalizację, której nie powinien był robić. Obejście błędu kompilatora jest w porządku, ale nie winiłbym za to niczego oprócz kompilatora.
Evan Carroll,

75

Stare pytanie, ale nikt nie wspomniał, że należy je zastąpić tablicami o zmiennej długości.

char arr[size];

zamiast

char *arr=alloca(size);

Jest w standardowym C99 i istniał jako rozszerzenie kompilatora w wielu kompilatorach.


5
Wspomniał o tym Jonathan Leffler w komentarzu do odpowiedzi Arthura Ulfeldta.
ninjalj

2
Rzeczywiście, ale pokazuje również, jak łatwo można go przeoczyć, ponieważ nie widziałem go pomimo przeczytania wszystkich odpowiedzi przed opublikowaniem.
Patrick Schlüter,

6
Jedna uwaga - są to tablice o zmiennej długości, a nie tablice dynamiczne. Te ostatnie są skalowalne i zwykle są implementowane na stercie.
Tim Čas

1
W Visual Studio 2015 kompilującym niektóre C ++ występuje ten sam problem.
ahcox,

2
Linus Torvalds nie lubi VLA w jądrze Linuksa . Począwszy od wersji 4.20 Linux powinien być prawie wolny od VLA.
Cristian Ciupitu

60

alokacja () jest bardzo przydatna, jeśli nie możesz użyć standardowej zmiennej lokalnej, ponieważ jej rozmiar musiałby zostać określony w czasie wykonywania i możesz absolutnie zagwarantować, że wskaźnik, który otrzymujesz z alokacji (), NIGDY nie zostanie użyty po powrocie tej funkcji .

Możesz być całkiem bezpieczny, jeśli tak

  • nie zwracaj wskaźnika ani niczego, co go zawiera.
  • nie przechowuj wskaźnika w żadnej strukturze przydzielonej na stercie
  • nie pozwól, aby jakikolwiek inny wątek używał wskaźnika

Prawdziwe niebezpieczeństwo wiąże się z szansą, że ktoś później naruszy te warunki. Mając to na uwadze, świetnie nadaje się do przekazywania buforów do funkcji formatujących w nich tekst :)


12
Funkcja VLA (tablica zmiennej długości) w C99 obsługuje zmienne lokalne o dynamicznym rozmiarze, bez wyraźnego wymagania użycia funkcji calca ().
Jonathan Leffler

2
neato! znaleziono więcej informacji w sekcji „3.4 Zmienna tablica długości” programumersheaven.com/2/Pointers-and-Arrays-page-2
Arthur Ulfeldt

1
Ale to nie różni się od obsługi wskaźników za pomocą zmiennych lokalnych. Można ich też oszukać ...
glglgl 18.10

2
@Jathanathan Leffler jedną rzeczą, którą możesz zrobić z przydziałem, ale nie możesz zrobić z VLA, jest użycie słowa kluczowego ograniczającego. W ten sposób: float * ograniczenie heavily_used_arr = przydzielanie (sizeof (float) * size); zamiast float heavily_used_arr [rozmiar]. Może to pomóc niektórym kompilatorom (w moim przypadku gcc 4.8) zoptymalizować zestaw, nawet jeśli rozmiar jest stałą kompilacji. Zobacz moje pytanie na ten temat: stackoverflow.com/questions/19026643/using-restrict-with-arrays
Piotr Lopusiewicz

@JathanathanLeffler VLA jest lokalny dla bloku, który go zawiera. Z drugiej strony alloca()przydziela pamięć, która trwa do końca funkcji. Oznacza to, że wydaje się, że nie ma prostego, wygodnego tłumaczenia na VLA z f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }. Jeśli uważasz, że możliwe jest automatyczne tłumaczenie zastosowań allocaVLA na zastosowania, ale wymaga więcej niż komentarza do opisania, w jaki sposób, mogę zadać to pytanie.
Pascal Cuoq

40

Jak zauważono w tym poście , istnieje kilka powodów, dla których używanie allocamoże być uważane za trudne i niebezpieczne:

  • Nie wszystkie kompilatory obsługują alloca.
  • Niektóre kompilatory allocaróżnie interpretują zamierzone zachowanie , więc przenośność nie jest gwarantowana nawet między kompilatorami, które je obsługują.
  • Niektóre implementacje są błędne.

24
Jedną rzeczą, o której wspomniałem w tym linku, której nie ma nigdzie indziej na tej stronie, jest to, że używana funkcja alloca()wymaga osobnych rejestrów do przechowywania wskaźnika stosu i wskaźnika ramki. Na procesorach x86> = 386 wskaźnik stosu ESPmoże być użyty do obu, zwalniając EBP- chyba że alloca()zostanie użyty.
j_random_hacker

10
Kolejny dobry punkt na tej stronie jest to, że chyba uchwytami generator kodu kompilator jest to jako szczególny przypadek, f(42, alloca(10), 43);mógł upaść ze względu na możliwość, że wskaźnik stosu jest regulowana przez alloca() po co najmniej jeden z argumentów jest popychany na nim.
j_random_hacker

3
Wydaje się, że link do niego został napisany przez Johna Levine'a - koleś, który napisał „Linkery i programy ładujące”, zaufałbym temu, co on powiedział.
user318904,

3
Połączony post jest odpowiedzią na post Johna Levine'a.
A. Wilcox,

6
Pamiętaj, że wiele się zmieniło od 1991 roku. Wszystkie nowoczesne kompilatory C (nawet w 2009 roku) muszą traktować alkohole jako szczególny przypadek; jest to funkcja wewnętrzna, a nie zwykła, i może nawet nie wywoływać funkcji. Tak więc kwestia alokacji parametru (która pojawiła się w K&R C od lat siedemdziesiątych) nie powinna być teraz problemem. Więcej szczegółów w komentarzu, który napisałem na temat odpowiedzi Tony'ego D
greggo,

26

Jednym z problemów jest to, że nie jest standardem, chociaż jest szeroko obsługiwany. Gdy inne rzeczy są takie same, zawsze używałbym standardowej funkcji zamiast wspólnego rozszerzenia kompilatora.


21

nadal odradza się używanie alokacji, dlaczego?

Nie dostrzegam takiego konsensusu. Wiele silnych zalet; kilka wad:

  • C99 zapewnia tablice o zmiennej długości, które często byłyby stosowane preferencyjnie, ponieważ notacja jest bardziej spójna z tablicami o stałej długości i ogólnie intuicyjnie
  • wiele systemów ma mniej ogólnej pamięci / przestrzeni adresowej dostępnej dla stosu niż dla stosu, co czyni program nieco bardziej podatnym na wyczerpanie pamięci (poprzez przepełnienie stosu): może to być postrzegane jako dobre lub złe - jedno z powodów, dla których stos nie rośnie automatycznie w taki sam sposób, jak sterty, jest zapobieganie, aby niekontrolowane programy miały tak niekorzystny wpływ na całą maszynę
  • w przypadku zastosowania w bardziej lokalnym zasięgu (takim jak pętla whilelub for) lub w kilku zakresach pamięć gromadzi się w zależności od iteracji / zakresu i nie jest zwalniana do momentu wyjścia z funkcji: kontrastuje to z normalnymi zmiennymi zdefiniowanymi w zakresie struktury kontroli (np. for {int i = 0; i < 2; ++i) { X }zgromadziłaby allocapamięć-X wymaganą w X, ale pamięć dla macierzy o stałej wielkości byłaby ponownie przetwarzana dla każdej iteracji).
  • nowoczesne kompilatory zazwyczaj nie inlinedziałają wzywające alloca, ale jeśli je wymusisz, allocanastąpi to w kontekście dzwoniących (tzn. stos nie zostanie zwolniony, dopóki dzwoniący nie powróci)
  • dawno temu allocaprzeszedł z nieprzenośnej funkcji / hacka do standardowego rozszerzenia, ale pewne negatywne postrzeganie może się utrzymywać
  • czas życia jest powiązany z zakresem funkcji, który może, ale nie musi, odpowiadać programiście lepiej niż mallocwyraźna kontrola
  • konieczność użycia malloczachęca do myślenia o dealokacji - jeśli jest to zarządzane za pomocą funkcji otoki (np. WonderfulObject_DestructorFree(ptr)), wówczas funkcja zapewnia punkt dla operacji czyszczenia implementacji (takich jak zamykanie deskryptorów plików, zwalnianie wewnętrznych wskaźników lub przeprowadzanie rejestrowania) bez wyraźnych zmian w kliencie kod: czasami jest to miły model do konsekwentnego przyjmowania
    • w tym pseudo-OO stylu programowania jest rzeczą naturalną chcieć czegoś podobnego WonderfulObject* p = WonderfulObject_AllocConstructor();- jest to możliwe, gdy „konstruktor” jest funkcją zwracającą mallocpamięć (ponieważ pamięć pozostaje przydzielona po zwróceniu przez funkcję wartości do zapamiętania p), ale nie jeśli używa „konstruktora”alloca
      • wersja makr WonderfulObject_AllocConstructormoże to osiągnąć, ale „makra są złe”, ponieważ mogą one powodować konflikty między sobą i kodem innym niż makr oraz tworzyć niezamierzone zamiany i wynikające z tego trudne do zdiagnozowania problemy
    • freeValGrind, Purify itp. mogą wykryć brakujące operacje, ale nie zawsze można wykryć brakujące wywołania „destruktora” - jedna bardzo niewielka korzyść w zakresie egzekwowania zamierzonego użycia; niektóre alloca()implementacje (takie jak GCC) używają wbudowanego makra alloca(), więc podstawienie biblioteki diagnostycznej wykorzystania pamięci w czasie wykonywania nie jest możliwe w taki sposób, jak w przypadku malloc/ realloc/ free(np. ogrodzenie elektryczne)
  • niektóre implementacje mają subtelne problemy: na przykład z podręcznika systemu Linux:

    W wielu systemach nie można używać funkcji wywoływania () na liście argumentów wywołania funkcji, ponieważ przestrzeń stosu zarezerwowana przez funkcję wywoływania pojawiłaby się na stosie w środku miejsca dla argumentów funkcji.


Wiem, że to pytanie jest oznaczone jako C, ale jako programista C ++ pomyślałem, że użyję C ++ do zilustrowania potencjalnej użyteczności alloca: poniższy kod (i tutaj w ideone ) tworzy wektor śledzący typy polimorficzne o różnej wielkości, które są przydzielane stosowo (z czas życia związany z funkcją return) zamiast przydzielonej sterty.

#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
    }
}

nie +1 z powodu podejrzanego, osobliwego sposobu obsługi kilku typów :-(
einpoklum

@einpoklum: Cóż, to głęboko pouczające ... dzięki.
Tony Delroy

1
Pozwólcie, że sformułuję: To bardzo dobra odpowiedź. Do tego stopnia, że ​​myślę, że sugerujesz, aby ludzie używali swego rodzaju kontr-wzorca.
einpoklum

Komentarz ze strony Linuksa jest bardzo stary i jestem pewien, że jest przestarzały. Wszystkie współczesne kompilatory wiedzą, czym jest przydzielanie (), i nie potkną się o takie sznurowadła. W starym K&R C, (1) wszystkie funkcje używały wskaźników ramek (2) Wszystkie wywołania funkcji to {push args on stack} {call func} {add # n, sp}. Alga była funkcją lib, która po prostu podniosła stos, kompilator nawet nie wiedział o tym. (1) i (2) nie są już prawdą, więc alokacja nie może działać w ten sposób (teraz jest to funkcja wewnętrzna). W starym C wywołanie alokacji w trakcie wypychania argumentów oczywiście złamałoby również te założenia.
greggo

4
Jeśli chodzi o przykład, ogólnie martwię się o coś, co wymaga always_inline, aby uniknąć uszkodzenia pamięci ....
greggo

14

Wszystkie pozostałe odpowiedzi są poprawne. Jeśli jednak rzecz, którą chcesz przydzielić, alloca()jest stosunkowo niewielka, uważam, że jest to dobra technika, która jest szybsza i wygodniejsza niż używanie malloc()lub w inny sposób.

Innymi słowy, alloca( 0x00ffffff )jest niebezpieczny i może spowodować przepełnienie, dokładnie tak samo jak char hugeArray[ 0x00ffffff ];jest. Bądź ostrożny i rozsądny, a wszystko będzie dobrze.


12

Wiele interesujących odpowiedzi na to „stare” pytanie, nawet niektóre stosunkowo nowe odpowiedzi, ale nie znalazłem żadnej, która wspomniałaby o tym ....

Przy prawidłowym i ostrożnym stosowaniu konsekwentne stosowanie alloca() (być może w całej aplikacji) do obsługi małych przydziałów o zmiennej długości (lub VLA C99, jeśli są dostępne) może prowadzić do niższego ogólnego wzrostu stosu niż w innym przypadku równoważna implementacja z wykorzystaniem dużych lokalnych tablic o stałej długości . alloca()Może więc być dobry dla twojego stosu, jeśli użyjesz go ostrożnie.

Znalazłem ten cytat w ... OK, zrobiłem go. Ale naprawdę, pomyśl o tym ....

@j_random_hacker ma rację w swoich komentarzach pod innymi odpowiedziami: Unikanie użycia alloca()na rzecz przewymiarowanych tablic lokalnych nie czyni twojego programu bezpieczniejszym przed przepełnieniem stosu (chyba że twój kompilator jest wystarczająco stary, aby umożliwić wstawianie funkcji, które alloca()w takim przypadku należy zastosować uaktualnić lub, chyba że używasz alloca()pętli wewnętrznych, w takim przypadku nie powinieneś ... nie używać alloca()pętli wewnętrznych).

Pracowałem w środowiskach desktop / server i systemach wbudowanych. Wiele systemów wbudowanych w ogóle nie korzysta ze sterty (nawet nie łączy się z obsługą), z powodów obejmujących przekonanie, że pamięć dynamicznie przydzielana jest zła z powodu ryzyka wycieków pamięci w aplikacji, która nigdy nie zawsze restartuje się przez lata lub bardziej rozsądne uzasadnienie, że pamięć dynamiczna jest niebezpieczna, ponieważ nie można z całą pewnością stwierdzić, że aplikacja nigdy nie podzieli stosu do momentu wyczerpania fałszywej pamięci. Dlatego wbudowanym programistom pozostaje niewiele alternatyw.

alloca() (lub VLA) może być właściwym narzędziem do pracy.

Raz po raz widziałem, jak programista sprawia, że ​​bufor przydzielany przez stos jest „wystarczająco duży, aby obsłużyć każdą możliwą sprawę”. W głęboko zagnieżdżonym drzewie wywołań wielokrotne stosowanie tego wzorca (anty -?) Prowadzi do przesadzonego użycia stosu. (Wyobraź sobie, że drzewo wywołań ma 20 poziomów głębokości, gdzie na każdym poziomie z różnych powodów funkcja ślepo przesadza bufor 1024 bajtów „po prostu dla bezpieczeństwa”, gdy generalnie użyje tylko 16 lub mniej z nich, i tylko w bardzo w rzadkich przypadkach można użyć więcej.) Alternatywą jest użyciealloca()lub VLA i alokuj tylko tyle miejsca na stosie, ile potrzebuje Twoja funkcja, aby uniknąć niepotrzebnego obciążania stosu. Mamy nadzieję, że gdy jedna funkcja w drzewie połączeń wymaga alokacji większej niż normalna, inne w drzewie połączeń nadal używają swoich normalnych małych alokacji, a ogólne użycie stosu aplikacji jest znacznie mniejsze niż w przypadku, gdy każda funkcja ślepo nadmiernie alokuje bufor lokalny .

Ale jeśli zdecydujesz się użyć alloca()...

Na podstawie innych odpowiedzi na tej stronie wydaje się, że VLA powinny być bezpieczne (nie łączą alokacji stosu, jeśli są wywoływane z pętli), ale jeśli używasz alloca(), uważaj, aby nie używać go wewnątrz pętli i upewnij się, że nie można wstawić funkcji, jeśli istnieje szansa, że ​​zostanie wywołana w pętli innej funkcji.


Zgadzam się z tym punktem. Niebezpieczne alloca()jest prawda, ale to samo można powiedzieć o przeciekach pamięci malloc()(dlaczego więc nie użyć GC? alloca()przy ostrożnym stosowaniu może być bardzo przydatne, aby zmniejszyć rozmiar stosu.
Felipe Tonello,

Kolejny dobry powód, aby nie używać pamięci dynamicznej, zwłaszcza we wbudowanym: jest to bardziej skomplikowane niż trzymanie się stosu. Korzystanie z pamięci dynamicznej wymaga specjalnych procedur i struktur danych, podczas gdy na stosie (dla uproszczenia) kwestią jest dodanie / odjęcie większej liczby od wskaźnika stosu.
tehftw

Sidenote: Przykład „korzystanie ze stałego bufora [MAX_SIZE]” podkreśla, dlaczego zasada nadpisywania pamięci działa tak dobrze. Programy przydzielają pamięć, której nigdy nie mogą dotknąć, z wyjątkiem granic długości bufora. To w porządku, że Linux (i inne systemy operacyjne) tak naprawdę nie przypisują strony pamięci do jej pierwszego użycia (w przeciwieństwie do malloc'd). Jeśli bufor jest większy niż jedna strona, program może użyć tylko pierwszej strony i nie marnować reszty pamięci fizycznej.
Katastic Voyage

@KatasticVoyage O ile MAX_SIZE nie jest większy (lub co najmniej równy) rozmiarowi strony pamięci wirtualnej systemu, twój argument nie trzyma wody. Również w systemach wbudowanych bez pamięci wirtualnej (wiele wbudowanych MCU nie ma MMU), zasada nadpisywania pamięci może być dobra z punktu widzenia „upewnij się, że Twój program będzie działał we wszystkich sytuacjach”, ale to zapewnienie wiąże się z ceną wielkości stosu musi być podobnie przydzielony, aby obsługiwał tę nadpisaną politykę pamięci. W niektórych systemach wbudowanych jest to cena, której niektórzy producenci tanich produktów nie są skłonni zapłacić.
phonetagger

11

Wszyscy już wskazywali na wielką rzecz, jaką jest potencjalne niezdefiniowane zachowanie po przepełnieniu stosu, ale powinienem wspomnieć, że środowisko Windows ma świetny mechanizm do wychwytywania tego za pomocą wyjątków strukturalnych (SEH) i ochrony stron. Ponieważ stos rośnie tylko w razie potrzeby, strony ochronne znajdują się w nieprzydzielonych obszarach. Jeśli przydzielisz do nich (poprzez przepełnienie stosu), zgłoszony zostanie wyjątek.

Możesz złapać ten wyjątek SEH i wywołać _resetstkoflw, aby zresetować stos i kontynuować na wesołej drodze. Nie jest idealny, ale jest to kolejny mechanizm pozwalający przynajmniej wiedzieć, że coś poszło nie tak, gdy coś trafi w wentylator. * nix może mieć coś podobnego, czego nie jestem świadomy.

Zalecam ograniczenie maksymalnego rozmiaru alokacji poprzez zawinięcie alokacji i śledzenie jej wewnętrznie. Jeśli naprawdę nie lubisz tego, możesz rzucić niektórymi wartownikami na górze swojej funkcji, aby śledzić wszelkie alokacje przydziałów w zakresie funkcji i sprawdzić, czy jest to poprawne względem maksymalnej dozwolonej kwoty dla twojego projektu.

Oprócz tego, że nie zezwala się na wycieki pamięci, alokacja nie powoduje fragmentacji pamięci, co jest dość ważne. Nie sądzę, że alga jest złą praktyką, jeśli używasz go inteligentnie, co zasadniczo jest prawdą we wszystkim. :-)


Problem polega na tym, że alloca()może wymagać tyle miejsca, że ​​wskaźnik stosu wyląduje na stercie. Dzięki temu atakujący, który może kontrolować rozmiar alloca()i dane, które trafiają do tego bufora, może nadpisać stertę (co jest bardzo złe).
12431234123412341234123

SEH to tylko system Windows. To świetnie, jeśli dbasz tylko o kod działający w systemie Windows, ale jeśli kod musi być wieloplatformowy (lub jeśli piszesz kod, który działa tylko na platformie innej niż Windows), nie możesz polegać na SEH.
George

10

alokacja () jest ładna i wydajna ... ale jest również głęboko zepsuta.

  • zepsuty zakres działania (zakres funkcji zamiast zakresu bloku)
  • używanie niespójnego wskaźnika nie powinno być zwolnione, ponieważ wskaźnik wyskakujący z malloc ( przydzielania () nie powinien być uwolniony, odtąd musisz śledzić, skąd pochodzą twoje wskaźniki do free () tylko tych, które otrzymałeś przy pomocy malloc () )
  • złe zachowanie, gdy używasz również inlinizacji (zakres czasami przechodzi do funkcji dzwoniącego, w zależności od tego, czy odbiorca jest wstawiony, czy nie).
  • brak kontroli granicy stosu
  • niezdefiniowane zachowanie w przypadku awarii (nie zwraca NULL jak malloc ... a co oznacza awaria, ponieważ i tak nie sprawdza granic stosu ...)
  • nie jest standardem

W większości przypadków można go zastąpić przy użyciu zmiennych lokalnych i wielkości głównej. Jeśli jest używany do dużych obiektów, umieszczenie ich na stosie jest zwykle bezpieczniejszym pomysłem.

Jeśli naprawdę potrzebujesz C, możesz użyć VLA (brak vla w C ++, szkoda). Są o wiele lepsze niż przydziała () pod względem zachowania zakresu i spójności. Widzę, że VLA jest rodzajem poprawnie wykonanej metody splita () .

Oczywiście lokalna struktura lub tablica wykorzystująca większość potrzebnej przestrzeni jest jeszcze lepsza, a jeśli nie masz takiej alokacji sterty głównej za pomocą zwykłego malloc (), prawdopodobnie jest to rozsądne. Nie widzę żadnego rozsądnego przypadku użycia, w którym naprawdę potrzebujesz albo funkcji Almla (), albo VLA.


Nie widzę powodu, dla którego głosowano (bez komentarza)
gd1

Tylko nazwy mają zakres. allocanie tworzy nazwy, tylko zakres pamięci, który ma żywotność .
ciekawy,

@curiousguy: bawisz się tylko słowami. W przypadku zmiennych automatycznych mógłbym równie dobrze mówić o żywotności pamięci podstawowej, ponieważ pasuje ona do zakresu nazwy. W każdym razie problemem nie jest to, jak to nazywamy, ale niestabilność czasu życia / zakresu pamięci zwróconej przez przydział i wyjątkowe zachowanie.
kriss,

2
Chciałbym, żeby Alga miała odpowiednią „freeę”, ze specyfikacją, że wywołanie „freea” cofnęłoby skutki „algi”, która utworzyła obiekt i wszystkie kolejne, oraz wymaganie, aby pamięć „alokacja" w ramach funkcji musiała być w nim także „wolnym”. Umożliwiłoby to prawie wszystkim implementacjom obsługę przydziału / freea w zgodny sposób, złagodziłoby podstawowe problemy i ogólnie uczyniłoby wszystko znacznie czystszym.
supercat

2
@ supercat - Też tego życzę. Z tego powodu (i więcej), używam warstwę abstrakcji (głównie makra i funkcje inline), tak, że nie zawsze zadzwonić allocalub malloclub freebezpośrednio. Mówię takie rzeczy jak {stack|heap}_alloc_{bytes,items,struct,varstruct}i {stack|heap}_dealloc. Więc heap_deallocpo prostu dzwoni freei stack_deallocnie ma opcji. W ten sposób alokacje stosu można łatwo obniżyć do alokacji sterty, a intencje są również bardziej jasne.
Todd Lehman

9

Dlatego:

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

Nie żeby ktokolwiek napisał ten kod, ale argument wielkości, do którego przekazujesz, allocaprawie na pewno pochodzi z jakiegoś rodzaju danych wejściowych, które mogą złośliwie dążyć do doprowadzenia twojego programu do allocaczegoś takiego. W końcu, jeśli rozmiar nie jest oparty na danych wejściowych lub nie ma możliwości być duży, to dlaczego nie zadeklarowałeś małego lokalnego bufora o stałej wielkości?

Praktycznie cały kod używający allocai / lub C99 vlas ma poważne błędy, które doprowadzą do awarii (jeśli masz szczęście) lub kompromisu uprzywilejowania (jeśli nie masz szczęścia).


1
Świat może nigdy nie wiedzieć. :( To powiedziawszy, mam nadzieję, że możesz wyjaśnić moje pytanie alloca. Powiedziałeś, że prawie cały kod, który go używa, zawiera błąd, ale planowałem go użyć; normalnie zignorowałbym takie twierdzenie, ale przychodzę od ciebie nie zrobię. Piszę maszynę wirtualną i chciałbym przydzielić zmienne, które nie uciekają od funkcji na stosie, zamiast dynamicznie, z powodu ogromnego przyspieszenia. Czy istnieje alternatywa podejście, które ma tę samą charakterystykę wydajności? Wiem, że mogę zbliżyć się do pul pamięci, ale wciąż nie jest to tak tanie. Co byś zrobił?
GManNickG

7
Wiesz, co jest niebezpieczne? To: *0 = 9;NIESAMOWITE !!! Wydaje mi się, że nigdy nie powinienem używać wskaźników (a przynajmniej ich wyłuskiwać). Err, czekaj. Mogę przetestować, czy jest zerowy. Hmm Chyba mogę też przetestować rozmiar pamięci, którą chcę przydzielić alloca. Dziwny człowiek Dziwne.
Thomas Eding,

7
*0=9;nie jest poprawny C. Jeśli chodzi o testowanie rozmiaru, który przekazujesz alloca, przetestuj go z czym? Nie ma możliwości poznania limitu, a jeśli zamierzasz go przetestować na niewielkim ustalonym, znanym bezpiecznym rozmiarze (np. 8k), możesz równie dobrze użyć tablicy o ustalonym rozmiarze na stosie.
R .. GitHub ZATRZYMAJ POMOC LODOWĄ

7
Problem z twoim argumentem „albo rozmiar jest wystarczająco mały, albo zależy od danych wejściowych, a zatem może być arbitralnie duży”, jak widzę, polega na tym, że dotyczy on równie mocno rekurencji. Praktycznym kompromisem (w obu przypadkach) jest założenie, że jeśli rozmiar jest ograniczony, small_constant * log(user_input)to prawdopodobnie mamy wystarczającą ilość pamięci.
j_random_hacker

1
Rzeczywiście, zidentyfikowałeś JEDEN przypadek, w którym użyteczne jest VLA / alokacja: algorytmy rekurencyjne, w których maksymalna przestrzeń wymagana w dowolnej ramce wywołania może być tak duża jak N, ale gdzie suma potrzebnej przestrzeni na wszystkich poziomach rekurencji wynosi N lub pewna funkcja z N, który nie rośnie szybko.
R .. GitHub ZATRZYMAJ LÓD

9

Nie sądzę, żeby ktokolwiek o tym wspominał: użycie alokacji w funkcji utrudni lub wyłączy niektóre optymalizacje, które mogłyby zostać zastosowane w funkcji, ponieważ kompilator nie może znać rozmiaru ramki stosu funkcji.

Na przykład częstą optymalizacją kompilatorów C jest wyeliminowanie użycia wskaźnika ramki w funkcji, dostęp do ramki jest wykonywany względem wskaźnika stosu; więc jest jeszcze jeden rejestr do ogólnego użytku. Ale jeśli wywoływany jest alokacja w obrębie funkcji, różnica między sp i fp będzie nieznana dla części funkcji, więc tej optymalizacji nie można wykonać.

Biorąc pod uwagę rzadkość jego użycia i jego zacieniony status jako funkcji standardowej, projektanci kompilatorów całkiem prawdopodobnie wyłączają jakąkolwiek optymalizację, która mogłaby powodować problemy z przydziałem, gdyby zajęło więcej niż trochę wysiłku, aby działał z przydziałem.

AKTUALIZACJA: Ponieważ lokalne tablice o zmiennej długości zostały dodane do C, a ponieważ stanowią one bardzo podobne problemy z generowaniem kodu w kompilatorze jak przydział, widzę, że „rzadkość użycia i zacieniony status” nie ma zastosowania do mechanizmu bazowego; ale nadal podejrzewałbym, że użycie alokacji lub VLA ma tendencję do kompromitowania generowania kodu w funkcji, która ich używa. Z radością powitam wszelkie opinie projektantów kompilatorów.


1
Tablice o zmiennej długości nigdy nie zostały dodane do C ++.
Nir Friedman,

@NirFriedman Rzeczywiście. Myślę, że istniała lista funkcji Wikipedii, która była oparta na starej propozycji.
greggo,

> W dalszym ciągu podejrzewałbym, że użycie alkoholi lub VLA ma tendencję do kompromisu w generowaniu kodu. Sądzę, że użycie alokacji wymaga wskaźnika ramki, ponieważ wskaźnik stosu porusza się w sposób, który nie jest oczywisty w czasie kompilacji. alokację można wywoływać w pętli, aby nadal pobierać więcej pamięci stosu lub z obliczonym rozmiarem czasu wykonywania itp. Jeśli istnieje wskaźnik ramki, wygenerowany kod ma stabilne odniesienie do miejscowych, a wskaźnik stosu może robić, co chce; nie jest używane.
Kaz

8

Jedną z pułapek allocajest to, że longjmpto przewija.

To znaczy, jeśli zapiszesz kontekst setjmp, a następnie allocatrochę pamięci, a następnie longjmpdo kontekstu, możesz stracić allocapamięć. Wskaźnik stosu powrócił tam, gdzie był, więc pamięć nie jest już zarezerwowana; wywołanie funkcji lub wykonanie innej allocaspowoduje zatarcie oryginału alloca.

Aby wyjaśnić, konkretnie mam na myśli sytuację, w której longjmpnie wraca z funkcji, w której allocamiała miejsce! Zamiast tego funkcja zapisuje kontekst za pomocą setjmp; następnie przydziela pamięć allocai na końcu odbywa się longjmp w tym kontekście. allocaPamięć tej funkcji nie jest całkowicie uwolniona; tylko cała pamięć przydzielona od czasu setjmp. Oczywiście mówię o zaobserwowanym zachowaniu; żaden taki wymóg nie jest udokumentowany w stosunku do żadnego, allocaktóry znam.

Dokumentacja koncentruje się zwykle na koncepcji, że allocapamięć jest powiązana z aktywacją funkcji , a nie z żadnym blokiem; że wiele wywołań po allocaprostu pobiera więcej pamięci stosu, która jest zwalniana po zakończeniu funkcji. Skąd; pamięć jest faktycznie związana z kontekstem procedury. Kiedy kontekst jest przywracany longjmp, to samo dotyczy allocastanu poprzedniego . Jest to konsekwencja użycia samego rejestru wskaźników stosu do alokacji, a także (koniecznie) zapisania i przywrócenia w jmp_buf.

Nawiasem mówiąc, to, jeśli działa w ten sposób, zapewnia wiarygodny mechanizm celowego uwalniania pamięci, która została przydzielona alloca.

Natknąłem się na to jako główną przyczynę błędu.


1
Tak właśnie powinno być - longjmpwraca i sprawia, że ​​program zapomina o wszystkim, co wydarzyło się na stosie: wszystkie zmienne, wywołania funkcji itp. I allocajest jak tablica na stosie, więc oczekuje się, że zostaną zablokowane jak wszystko inne na stosie.
tehftw

1
man allocadał następujące zdanie: „Ponieważ przestrzeń przydzielona przez alokację () jest przydzielona w ramce stosu, przestrzeń ta jest automatycznie zwalniana, jeśli powrót funkcji zostanie przeskoczony przez wywołanie longjmp (3) lub siglongjmp (3).”. Jest więc udokumentowane, że pamięć przydzielona z allocazostaje zablokowana po longjmp.
tehftw

@tehftw Opisana sytuacja występuje bez przeskakiwania powrotu funkcji longjmp. Funkcja docelowa jeszcze nie powróciła. To uczynił setjmp, allocaa następnie longjmp. longjmpMoże przewinąć allocaplecy państwa do tego, co było w setjmpczasie. Oznacza to, że wskaźnik przesuwany przez allocama ten sam problem co zmienna lokalna, która nie została zaznaczona volatile!
Kaz

3
Nie rozumiem, dlaczego miałoby to być nieoczekiwane. Kiedy setjmpwtedy alloca, a potem wtedy longjmp, allocaprzewijanie do tyłu jest normalne. Chodzi o longjmpto, aby wrócić do stanu, w którym został zapisany setjmp!
tehftw

@tehftw Nigdy nie widziałem tej konkretnej interakcji udokumentowanej. Dlatego nie można na nim polegać inaczej niż poprzez badanie empiryczne za pomocą kompilatorów.
Kaz

7

Miejsce, w którym alloca()jest szczególnie niebezpieczne niż malloc()jądro - jądro typowego systemu operacyjnego ma miejsce na stos o stałej wielkości, zakodowane na stałe w jednym z jego nagłówków; nie jest tak elastyczny jak stos aplikacji. Wywołanie alloca()z nieuzasadnionym rozmiarem może spowodować awarię jądra. Niektóre kompilatory ostrzegają przed użyciem alloca()(a nawet VLA) pod pewnymi opcjami, które powinny być włączone podczas kompilacji kodu jądra - tutaj lepiej jest alokować pamięć na stercie, która nie jest ustalona przez zakodowany na stałe limit.


7
alloca()nie jest bardziej niebezpieczny niż int foo[bar];gdziekolwiek bardowolna liczba całkowita.
Todd Lehman,

@ToddLehman Zgadza się, i właśnie z tego powodu od kilku lat zbanowaliśmy VLA w jądrze i jesteśmy wolni od VLA od 2018 roku :-)
Chris Down

6

Jeśli przypadkowo napiszesz poza blok przydzielony alloca(na przykład z powodu przepełnienia bufora), nadpiszesz adres zwrotny swojej funkcji, ponieważ ten znajduje się „nad” na stosie, tj. Po przydzielonym bloku.

_alloca blok na stosie

Konsekwencje tego są dwojakie:

  1. Program zawiesi się w spektakularny sposób i nie będzie możliwe określenie, dlaczego lub gdzie się zawiesił (stos najprawdopodobniej odwija ​​się do losowego adresu z powodu nadpisanego wskaźnika ramki).

  2. Powoduje to, że przepełnienie bufora jest wielokrotnie bardziej niebezpieczne, ponieważ złośliwy użytkownik może stworzyć specjalny ładunek, który zostałby umieszczony na stosie i dlatego może zostać wykonany.

W przeciwieństwie do tego, jeśli piszesz poza blokiem na stercie, „po prostu” dostajesz uszkodzenie stosu. Program prawdopodobnie zostanie nieoczekiwanie zakończony, ale odpowiednio odwija ​​stos, zmniejszając w ten sposób ryzyko wykonania złośliwego kodu.


11
Nic w tej sytuacji nie różni się dramatycznie od niebezpieczeństw przepełnienia bufora bufora o przydzielonych stosach o stałej wielkości. Niebezpieczeństwo to nie jest unikalne alloca.
fonetagger

2
Oczywiście nie. Ale proszę sprawdź oryginalne pytanie. Pytanie brzmi: jakie jest niebezpieczeństwo w allocaporównaniu z malloc(dlatego bufor nie ma stałej wielkości na stosie).
rustyx

Drobny punkt, ale stosy w niektórych systemach rosną w górę (np. 16-bitowe mikroprocesory PIC).
EBlake

5

Niestety w alloca()prawie tak niesamowitym tcc brakuje naprawdę wspaniałego . Gcc ma alloca().

  1. Sieje ziarno własnego zniszczenia. Z powrotem jako destruktor.

  2. Podobnie malloc()jak zwraca nieprawidłowy wskaźnik w przypadku awarii, co spowoduje segfault w nowoczesnych systemach z MMU (i mam nadzieję, że zrestartują te bez).

  3. W przeciwieństwie do zmiennych automatycznych można określić rozmiar w czasie wykonywania.

Działa dobrze z rekurencją. Możesz użyć zmiennych statycznych, aby osiągnąć coś podobnego do rekurencji ogona i użyć tylko kilku innych informacji do każdej iteracji.

Jeśli pchniesz zbyt głęboko, masz pewność segfault (jeśli masz MMU).

Zauważ, że malloc()nie oferuje nic więcej, ponieważ zwraca NULL (który również segfault, jeśli zostanie przypisany), gdy w systemie zabraknie pamięci. Tj. Wszystko, co możesz zrobić, to zwolnienie za kaucją lub po prostu spróbować przypisać to w jakikolwiek sposób.

Aby użyć malloc(), używam globałów i przypisuję im NULL. Jeśli wskaźnik nie ma wartości NULL, zwalniam go przed użyciem malloc().

Możesz również użyć realloc()jako ogólnego przypadku, jeśli chcesz skopiować istniejące dane. Musisz sprawdzić wskaźnik przed sprawdzeniem, czy zamierzasz skopiować lub połączyć po realloc().

3.2.5.2 Zalety przydziału


4
W rzeczywistości specyfikacja przydziału nie mówi, że zwraca niepoprawny wskaźnik przy błędzie (przepełnienie stosu), mówi, że ma niezdefiniowane zachowanie ... a dla malloc mówi, że zwraca NULL, a nie losowy nieprawidłowy wskaźnik (OK, optymalizacja implementacji pamięci w systemie Linux powoduje, że nieprzydatny).
kriss

@kriss Linux może zabić twój proces, ale przynajmniej nie zapuszcza się w nieokreślone zachowanie
craig65535

@ craig65535: wyrażenie niezdefiniowane zachowanie zwykle oznacza, że ​​to zachowanie nie jest zdefiniowane w specyfikacji C lub C ++. W żaden sposób nie będzie losowy ani niestabilny w danym systemie operacyjnym lub kompilatorze. Dlatego nie ma sensu kojarzyć UB z nazwą systemu operacyjnego, takiego jak „Linux” lub „Windows”. To nie ma z tym nic wspólnego.
kriss,

Próbowałem powiedzieć, że malloc zwracający wartość NULL lub, w przypadku Linuksa, dostęp do pamięci zabijający twój proces, jest lepszy niż niezdefiniowane zachowanie alby. Myślę, że musiałem źle odczytać twój pierwszy komentarz.
craig65535,

3

Procesy mają tylko ograniczoną ilość dostępnego miejsca na stosie - znacznie mniej niż ilość dostępnej pamięci malloc().

Korzystając z niej alloca(), znacznie zwiększasz swoje szanse na błąd przepełnienia stosu (jeśli masz szczęście lub niewytłumaczalną awarię, jeśli nie masz).


To zależy bardzo od aplikacji. Nie jest niczym niezwykłym, że aplikacja osadzona o ograniczonej pamięci ma rozmiar stosu większy niż sterta (jeśli nawet jest sterta).
EBlake

3

Niezbyt ładne, ale jeśli wydajność naprawdę ma znaczenie, możesz wstępnie przydzielić trochę miejsca na stosie.

Jeśli masz już maksymalny rozmiar bloku pamięci, którego potrzebujesz i chcesz zachować kontrolę przepełnienia, możesz zrobić coś takiego:

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

    (...)
}

12
Czy tablica char gwarantuje prawidłowe dopasowanie dla dowolnego typu danych? Alfa zapewnia taką obietnicę.
Juho Östman

@ JuhoÖstman: możesz użyć tablicy struct (lub dowolnego typu) zamiast char, jeśli masz problemy z wyrównaniem.
kriss

Nazywa się to tablicą o zmiennej długości . Obsługiwany jest w wersji C90 i nowszej, ale nie w C ++. Zobacz Czy mogę używać tablicy C o zmiennej długości w C ++ 03 i C ++ 11?
jww

3

Funkcja alokacji jest świetna i wszyscy naysayers po prostu rozpowszechniają FUD.

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

Array i parray są DOKŁADNIE takie same, przy DOKŁADNIE takich samych ryzykach. Mówienie, że jeden jest lepszy od drugiego, jest wyborem syntaktycznym, a nie technicznym.

Jeśli chodzi o wybór zmiennych stosu względem zmiennych stosu, istnieje wiele zalet w stosunku do długo działających programów używających stosu ponad stosu dla zmiennych o czasie życia w zakresie. Unikasz fragmentacji sterty i możesz uniknąć powiększania przestrzeni procesu za pomocą nieużywanej (nieużywalnej) przestrzeni sterty. Nie musisz go sprzątać. Możesz kontrolować alokację stosu w procesie.

Dlaczego to takie złe?


3

W rzeczywistości nie można zagwarantować, że skorzysta ze stosu. Rzeczywiście, implementacja alokacji gcc-2.95 przydziela pamięć ze sterty za pomocą samego malloc. Również implementacja jest błędna, może prowadzić do wycieku pamięci i nieoczekiwanego zachowania, jeśli wywołasz ją w bloku z dalszym użyciem goto. Nie, żeby powiedzieć, że nigdy nie powinieneś go używać, ale czasami alokacja prowadzi do większych kosztów ogólnych, niż to, co uwalnia.


Wygląda na to, że gcc-2.95 złamał alokację i prawdopodobnie nie może być bezpiecznie używany w programach, które tego wymagają alloca. Jak wyczyściłby pamięć, gdy longjmpjest używany do porzucania ramek, które to zrobiły alloca? Kiedy ktoś miałby dzisiaj używać gcc 2.95?
Kaz

2

IMHO, alokacja jest uważana za złą praktykę, ponieważ wszyscy boją się wyczerpać limit wielkości stosu.

Nauczyłem się wiele, czytając ten wątek i kilka innych linków:

Korzystam z alokacji głównie po to, aby moje zwykłe pliki C były kompilowane na msvc i gcc bez żadnych zmian, stylu C89, bez #ifdef _MSC_VER itp.

Dziękuję Ci ! Ten wątek zmusił mnie do zarejestrowania się na tej stronie :)


Pamiętaj, że na tej stronie nie ma czegoś takiego jak „wątek”. Przepełnienie stosu ma format pytań i odpowiedzi, a nie format wątku dyskusji. „Odpowiedź” nie jest jak „Odpowiedz” na forum dyskusyjnym; oznacza to, że faktycznie udzielasz odpowiedzi na pytanie, i nie powinien być używany do odpowiadania na inne odpowiedzi lub komentowania tematu. Gdy będziesz mieć co najmniej 50 powtórzeń, możesz dodawać komentarze , ale koniecznie przeczytaj „Kiedy nie powinienem komentować?” Sekcja. Przeczytaj stronę Informacje, aby lepiej zrozumieć format witryny.
Adi Inbar

1

Moim zdaniem, diva (), jeśli jest dostępna, powinna być używana tylko w ograniczony sposób. Bardzo podobna do użycia „goto”, całkiem spora liczba rozsądnych ludzi wykazuje dużą niechęć nie tylko do używania, ale także istnienia, funkcji diva ().

W przypadku użycia wbudowanego, gdy znany jest rozmiar stosu i można nałożyć ograniczenia poprzez konwencję i analizę wielkości alokacji, i gdzie kompilatora nie można zaktualizować do obsługi C99 +, użycie alga () jest w porządku i byłem znany z tego, że go używa.

Jeśli są dostępne, licencje VLA mogą mieć pewne zalety w stosunku do metody przydzielania (): kompilator może generować kontrole limitu stosu, które wychwycą dostęp poza granicami, gdy używany jest dostęp do stylu tablicy (nie wiem, czy którykolwiek kompilator to robi, ale może do zrobienia), a analiza kodu może ustalić, czy wyrażenia dostępu do tablicy są odpowiednio ograniczone. Należy pamiętać, że w niektórych środowiskach programowania, takich jak motoryzacja, sprzęt medyczny i awionika, analizy tej należy dokonać nawet dla tablic o stałych rozmiarach, zarówno automatycznych (na stosie), jak i przydziału statycznego (globalnego lub lokalnego).

W architekturach, które przechowują zarówno stos danych, jak i adresy zwrotne / wskaźniki ramek na stosie (z tego co wiem, to wszystko z nich), każda zmienna przydzielona do stosu może być niebezpieczna, ponieważ można pobrać adres zmiennej, a niesprawdzone wartości wejściowe mogą pozwolić wszelkiego rodzaju psoty.

Przenośność jest mniej istotna w zagnieżdżonej przestrzeni, ale jest to dobry argument przeciwko używaniu funkcji splita () poza ściśle kontrolowanymi okolicznościami.

Poza zagnieżdżoną przestrzenią, w celu zwiększenia wydajności, użyłem przydziałów () głównie w funkcjach rejestrowania i formatowania oraz w nierekurencyjnym skanerze leksykalnym, w którym struktury tymczasowe (przydzielane przy użyciu przydziału () są tworzone podczas tokenizacji i klasyfikacji, a następnie trwałe obiekt (przydzielony przez malloc ()) jest zapełniany przed powrotem funkcji. Użycie funkcji replacea () dla mniejszych struktur tymczasowych znacznie zmniejsza fragmentację, gdy przydzielany jest obiekt trwały.


1

Większość odpowiedzi tutaj w dużej mierze nie ma sensu: istnieje powód, dla którego używanie _alloca()jest potencjalnie gorsze niż zwykłe przechowywanie dużych obiektów na stosie.

Główna różnica między automatycznym przechowywaniem _alloca()polega na tym, że ten drugi ma dodatkowy (poważny) problem: przydzielony blok nie jest kontrolowany przez kompilator , więc nie ma możliwości, aby kompilator zoptymalizował go lub poddał recyklingowi.

Porównać:

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

z:

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

Problem z tym ostatnim powinien być oczywisty.


Czy masz jakieś praktyczne przykłady pokazujące różnicę między VLA a alloca(tak, mówię VLA, ponieważ allocajest więcej niż tylko twórcą tablic o wielkości statycznej)?
Ruslan

Istnieją przypadki użycia dla drugiego, których pierwszy nie obsługuje. Mogę chcieć mieć rekordy „n” po zakończeniu pracy pętli „n” razy - być może na liście połączonej lub w drzewie; ta struktura danych jest następnie usuwana, gdy funkcja ostatecznie powraca. Co nie znaczy, że
kodowałbym

1
Powiedziałbym, że „kompilator nie jest w stanie tego kontrolować”, ponieważ właśnie w ten sposób definiowana jest funkcja calca (); nowoczesne kompilatory wiedzą, co to jest przydział, i traktują go specjalnie; to nie tylko funkcja biblioteczna jak w latach 80-tych. C99 VLA są zasadniczo alokacjami z zakresem bloków (i lepszym typowaniem). Żadnej kontroli mniej więcej, tylko zgodność z inną semantyką.
greggo

@greggo: Jeśli jesteś zwycięzcą, chętnie usłyszę, dlaczego uważasz, że moja odpowiedź nie jest przydatna.
alecov

W C recykling nie jest zadaniem kompilatora, lecz biblioteką c (free ()). Alokacja () jest zwalniana po powrocie.
peterh - Przywróć Monikę

1

Nie sądzę, żeby ktokolwiek o tym wspominał, ale Alfa ma również poważne problemy z bezpieczeństwem, niekoniecznie związane z Malloc (chociaż problemy te pojawiają się także w przypadku tablic opartych na stosie, dynamicznych lub nie). Ponieważ pamięć jest przydzielana na stosie, przepełnienia / niedopełnienia bufora mają znacznie poważniejsze konsekwencje niż w przypadku samego malloc.

W szczególności adres zwrotny funkcji jest przechowywany na stosie. Jeśli ta wartość zostanie uszkodzona, twój kod może przejść do dowolnego wykonywalnego regionu pamięci. Kompilatory bardzo się starają, aby to utrudnić (w szczególności przez losowy układ adresów). Jest to jednak wyraźnie gorsze niż zwykłe przepełnienie stosu, ponieważ najlepszym przypadkiem jest SEGFAULT, jeśli zwracana wartość jest uszkodzona, ale może również zacząć wykonywać losowy fragment pamięci lub, w najgorszym przypadku, pewien obszar pamięci, który zagraża bezpieczeństwu twojego programu .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.