W jaki sposób dochodzi do przepełnienia stosu i jakie są najlepsze sposoby, aby temu zapobiec, lub sposoby, aby temu zapobiec, szczególnie na serwerach internetowych, ale inne przykłady również byłyby interesujące?
memory
stack-overflow
JasonMichael
źródło
źródło
Odpowiedzi:
Stos
W tym kontekście stos jest ostatnim wchodzącym, pierwszym wyjściowym buforem, w którym umieszczane są dane podczas działania programu. Last in, first out (LIFO) oznacza, że ostatnią rzeczą, którą włożysz, jest zawsze pierwsza rzecz, którą otrzymujesz - jeśli włożysz 2 elementy na stos, „A”, a następnie „B”, to pierwsza rzecz, którą wyskoczysz ze stosu będzie „B”, a następną rzeczą będzie „A”.
Kiedy wywołujesz funkcję w swoim kodzie, następna instrukcja po wywołaniu funkcji jest przechowywana na stosie oraz przestrzeń pamięci, która może zostać nadpisana przez wywołanie funkcji. Funkcja, którą wywołujesz, może zużywać więcej stosu dla swoich własnych zmiennych lokalnych. Po zakończeniu zwalnia używane miejsce na stosie zmiennych lokalnych, a następnie powraca do poprzedniej funkcji.
Przepełnienie stosu
Przepełnienie stosu ma miejsce, gdy zużyjesz więcej pamięci na stos, niż powinien zużywać Twój program. W systemach osadzonych możesz mieć tylko 256 bajtów na stos, a jeśli każda funkcja zajmuje 32 bajty, możesz mieć tylko wywołania funkcji 8 głęboko - funkcja 1 wywołuje funkcję 2, która wywołuje funkcję 3, która wywołuje funkcję 4 ... kto wywołuje funkcja 8, która wywołuje funkcję 9, ale funkcja 9 nadpisuje pamięć poza stosem. Może to spowodować nadpisanie pamięci, kodu itp.
Wielu programistów popełnia ten błąd, wywołując funkcję A, która następnie wywołuje funkcję B, która następnie wywołuje funkcję C, a następnie wywołuje funkcję A. Może to działać przez większość czasu, ale tylko raz błędne dane wejściowe spowodują, że będzie krążyć w tym kręgu na zawsze dopóki komputer nie wykryje przepełnienia stosu.
Przyczyną tego są również funkcje rekurencyjne, ale jeśli piszesz rekurencyjnie (tj. Twoja funkcja wywołuje samą siebie), musisz być tego świadomy i używać zmiennych statycznych / globalnych, aby zapobiec nieskończonej rekurencji.
Ogólnie system operacyjny i język programowania, którego używasz, zarządzają stosem i nie masz tego w rękach. Powinieneś spojrzeć na swój wykres wywołań (strukturę drzewa, która pokazuje z twojego głównego, co wywołuje każda funkcja), aby zobaczyć, jak głęboko sięgają wywołania funkcji i wykryć cykle i rekursję, które nie są zamierzone. Celowe cykle i rekurencja muszą być sztucznie sprawdzane, aby uzyskać błąd, jeśli wywołują się zbyt wiele razy.
Poza dobrymi praktykami programistycznymi, testami statycznymi i dynamicznymi, niewiele można zrobić na tych wysokopoziomowych systemach.
Systemy wbudowane
W świecie systemów wbudowanych, zwłaszcza w kodach o wysokiej niezawodności (motoryzacja, samoloty, przestrzeń kosmiczna) wykonujesz obszerne przeglądy i sprawdzanie kodu, ale również wykonujesz następujące czynności:
Języki i systemy wysokiego poziomu
Ale w językach wysokiego poziomu uruchamianych w systemach operacyjnych:
Serwery WWW
To zależy od posiadanej „piaskownicy”, czy możesz kontrolować, czy nawet widzieć stos. Jest duża szansa, że możesz traktować serwery internetowe tak, jak każdy inny język wysokiego poziomu i system operacyjny - to w dużej mierze poza twoimi rękami, ale sprawdź język i stos serwerów, których używasz. Możliwe jest na przykład wysadzenie stosu na serwerze SQL.
-Adam
źródło
Przepełnienie stosu w rzeczywistym kodzie występuje bardzo rzadko. Większość sytuacji, w których występuje, to rekurencje, w których zapomniano o zakończeniu. Może jednak rzadko występować w silnie zagnieżdżonych strukturach, np. Szczególnie dużych dokumentach XML. Jedyną prawdziwą pomocą jest tutaj refaktoryzacja kodu w celu użycia jawnego obiektu stosu zamiast stosu wywołań.
źródło
Większość ludzi powie ci, że przepełnienie stosu występuje z rekurencją bez ścieżki wyjścia - chociaż w większości jest to prawda, jeśli pracujesz z wystarczająco dużymi strukturami danych, nawet właściwa ścieżka wyjścia rekursji nie pomoże.
Niektóre opcje w tym przypadku:
źródło
Do przepełnienia stosu dochodzi, gdy Jeff i Joel chcą dać światu lepsze miejsce do uzyskiwania odpowiedzi na pytania techniczne. Jest już za późno, aby zapobiec przepełnieniu tego stosu. Ta „inna witryna” mogła temu zapobiec, nie będąc lichym. ;)
źródło
Nieskończona rekurencja jest powszechnym sposobem uzyskania błędu przepełnienia stosu. Aby zapobiec - zawsze upewnij się, że istnieje ścieżka wyjścia, która zostanie trafiona. :-)
Innym sposobem na przepełnienie stosu (przynajmniej w C / C ++) jest zadeklarowanie jakiejś ogromnej zmiennej na stosie.
To wystarczy.
źródło
Zwykle przepełnienie stosu jest wynikiem nieskończonego wywołania rekurencyjnego (biorąc pod uwagę zwykłą ilość pamięci w dzisiejszych standardowych komputerach).
Kiedy wywołujesz metodę, funkcję lub procedurę „standardowy” sposób lub wykonanie wywołania polega na:
Zwykle zajmuje to kilka bajtów, w zależności od liczby i typu parametrów, a także architektury maszyny.
Zobaczysz wtedy, że jeśli zaczniesz wykonywać wywołania rekurencyjne, stos zacznie rosnąć. Obecnie stos jest zwykle rezerwowany w pamięci w taki sposób, że rośnie w kierunku przeciwnym do stosu, więc przy dużej liczbie wywołań bez „powrotu” stos zaczyna się zapełniać.
Teraz, w starszych czasach przepełnienie stosu mogło wystąpić po prostu dlatego, że wyczerpałeś całą dostępną pamięć, tak po prostu. W przypadku modelu pamięci wirtualnej (do 4 GB w systemie X86), który był poza zakresem, więc zwykle, jeśli pojawi się błąd przepełnienia stosu, szukaj nieskończonego wywołania rekurencyjnego.
źródło
Co? Nikt nie kocha tych objętych nieskończoną pętlą?
źródło
Oprócz formy przepełnienia stosu, którą otrzymujesz z bezpośredniej rekursji (np.
Fibonacci(1000000)
), Bardziej subtelną jego formą, której doświadczyłem wiele razy, jest rekurencja pośrednia, w której funkcja wywołuje inną funkcję, która wywołuje inną, a następnie jedną z funkcje te ponownie wywołują pierwszą.Może się to często zdarzyć w funkcjach wywoływanych w odpowiedzi na zdarzenia, ale które same mogą generować nowe zdarzenia, na przykład:
W tym przypadku wywołanie to
ResizeWindow
może spowodowaćWindowSizeChanged()
ponowne wyzwolenie wywołania zwrotnego, które wywołujeResizeWindow
ponownie, aż do wyczerpania stosu. W takich sytuacjach często trzeba odłożyć odpowiedź na zdarzenie do momentu powrotu ramki stosu, np. Poprzez wysłanie wiadomości.źródło
Biorąc pod uwagę, że zostało to oznaczone jako „hacking”, podejrzewam, że „przepełnienie stosu”, do którego się odnosi, to przepełnienie stosu wywołań, a nie przepełnienie stosu wyższego poziomu, takie jak te, do których odwołuje się większość innych odpowiedzi tutaj. Tak naprawdę nie ma zastosowania do żadnych zarządzanych lub interpretowanych środowisk, takich jak .NET, Java, Python, Perl, PHP itp., W których aplikacje internetowe są zazwyczaj napisane, więc jedynym ryzykiem jest sam serwer sieciowy, który prawdopodobnie jest napisany w języku C lub C ++.
Sprawdź ten wątek:
/programming/7308/what-is-a-good-starting-point-for-learning-buffer-overflow
źródło
Odtworzyłem problem przepełnienia stosu podczas uzyskiwania najczęściej spotykanej liczby Fibonacciego, tj. 1, 1, 2, 3, 5 ..... więc obliczenia dla fib (1) = 1 lub fib (3) = 2 .. fib (n ) = ??.
dla n, powiedzmy, że będziemy zainteresowani - a co jeśli n = 100 000 to jaka będzie odpowiadająca mu liczba Fibonacciego?
Podejście z jedną pętlą jest jak poniżej -
to całkiem proste, a wynik jest -
Teraz innym podejściem, które zastosowałem, jest dzielenie i współbieżność za pomocą rekurencji
tj. Fib (n) = fib (n-1) + Fib (n-2), a następnie dalsza rekurencja dla n-1 i n-2 ..... do 2 i 1, która jest zaprogramowana jako -
Kiedy uruchomiłem kod dla n = 100 000, wynik jest następujący -
Powyżej widać, że tworzony jest StackOverflowError. Powodem tego jest zbyt wiele rekurencji, ponieważ -
Więc każdy wpis w stosie tworzy 2 kolejne wpisy i tak dalej ... co jest reprezentowane jako -
Ostatecznie zostanie utworzonych tak wiele wpisów, że system nie będzie w stanie obsłużyć stosu i wyrzucony zostanie StackOverflowError.
Dla Zapobiegania: Dla powyższej perspektywy przykładu - 1. Unikaj stosowania podejścia rekurencyjnego lub zmniejsz / ogranicz rekursję o jeden poziom, tak jak gdyby n jest zbyt duże, a następnie podziel n tak, aby system mógł obsłużyć jego limit. 2. Użyj innego podejścia, na przykład pętli, której użyłem w pierwszym przykładzie kodu. (Wcale nie zamierzam degradować Divide & Concur lub Recursion, ponieważ są to legendarne podejścia w wielu najbardziej znanych algorytmach .. moim zamiarem jest ograniczenie rekurencji lub trzymanie się z dala od niej, jeśli podejrzewam problemy z przepełnieniem stosu)
źródło