Sytuacja kobiet w IT w 2024 roku
16.05.20215 min
Tiago Antunes

Tiago AntunesTécnico Lisboa

Jak zrozumieć pamięć programu?

Dowiedz się więcej o stosie i stercie, żeby lepiej zrozumieć działanie pamięci programu.

Jak zrozumieć pamięć programu?

Kiedy kodujesz w językach takich jak np. C lub C ++, możesz operować pamięcią w bardziej niskopoziomowy sposób. Czasami stwarza to wiele problemów, których wcześniej nie doświadczyłeś: segfaulty. Te błędy są dość irytujące i mogą powodować wiele problemów. Często świadczą o tym, że używasz pamięci, której nie powinieneś używać.

Jednym z najczęstszych problemów jest dostęp do pamięci, która została już zwolniona. Jest to pamięć, którą albo zwolniłeś przy użyciu free, albo pamięć, którą Twój program automatycznie zwolnił, na przykład ze stosu.

Zrozumienie tego wszystkiego jest naprawdę proste i na pewno sprawi, że będziesz programował lepiej i inteligentniej.

Jak podzielona jest pamięć?


Pamięć jest podzielona na wiele segmentów. Dwa z najważniejszych dla tego artykułu to stos (stack) i sterta (heap). Stos jest miejscem, gdzie wstawia się dane w sposób uporządkowany, podczas gdy na stercie alokujesz pamięć, gdzie tylko możesz.

Stos pamięci działa w określony sposób i ma konkretne operacje, które to umożliwiają. Tutaj zapisywane są niektóre z rejestrów Twojego procesora. Tam również trafiają istotne informacje o Twoim programie - które funkcje są wywoływane, jakie zmienne stworzyłeś itp. Ta pamięć jest zarządzana przez program, a nie przez programistę.

Sterta jest często używana do przydzielania dużej ilości pamięci, która powinna istnieć tak długo, jak chce tego programista. To znaczy, że zadaniem programisty jest kontrolowanie wykorzystania pamięci na stercie. Podczas budowania złożonych programów, często trzeba przydzielać duże porcje pamięci i tam właśnie używa się sterty. Nazywamy to pamięcią dynamiczną.

Umieszczasz rzeczy na stercie za każdym razem, gdy używasz ‘malloc’ do przydzielania pamięci na coś. Każde inne połączenie, które wygląda jak int i;, to pamięć stosu. Świadomość tego jest bardzo ważna. Dzięki temu można łatwo znaleźć błędy w programie i łatwiej znajdować segfaulty.

Zrozumieć stos

Chociaż możesz o tym nie wiedzieć, Twój program nieustannie przydziela pamięć stosu, aby ta działała. Każda lokalna zmienna i każda funkcja, którą wywołujesz, ląduje właśnie tam. Dzięki temu możesz zrobić wiele rzeczy. W przypadku większości z nich nie chcesz, aby się stały - jak przepełnienie bufora i dostęp do niewłaściwej pamięci.

Jak to naprawdę działa?

Stos jest strukturą danych LIFO (Last-In-First-Out). Możesz go sobie wyobrazić, jako pudełko idealnie dopasowanych książek - ostatnia książka, którą umieścisz, jest pierwszą, którą wyjmiesz. Korzystając z tej struktury, program może łatwo zarządzać wszystkimi swoimi operacjami i zakresami za pomocą dwóch prostych operacji: push i pop.

Te dwie operacje są dokładnie sobie przeciwne. Push wstawia wartość na wierzch stosu. Pop bierze z tego ostatnią wartość.

Aby śledzić bieżące miejsce w pamięci, istnieje specjalny rejestr procesora o nazwie Stack Pointer. Za każdym razem, gdy trzeba coś zapisać - jak zmienną lub adres zwrotny z funkcji - przesuwa wskaźnik stosu w górę. Za każdym razem, gdy wychodzisz z funkcji, wyskakuje ona ze wskaźnika stosu do zapisanego adresu zwrotnego z funkcji. To proste!

Aby sprawdzić, czy zrozumiałeś, prześledź przykład (spróbuj sam znaleźć błąd ☺️):


Wszystko wygląda dobrze - dopóki tego nie uruchomisz. Gdy to zrobisz, nastąpi segfault. Dlaczego tak się dzieje? Wszystko jest na swoim miejscu! Z wyjątkiem... stosu.

Kiedy wywołamy funkcję ‘createArray’, wtedy stos:

  • zapisuje adres zwrotny,
  • tworzy ‘arr’ w pamięci stosu i to zwraca (tablica jest po prostu wskaźnikiem do miejsca w pamięci z informacjami o niej)
  • ale ponieważ nie używaliśmy ‘malloc’, zostaje on zapisany w pamięci stosu

Po zwróceniu wskaźnika, ponieważ nie mamy żadnej kontroli nad operacjami stosu, program pobiera informacje ze stosu i używa zgodnie z potrzebami. Kiedy spróbujemy wypełnić tablicę po wyjściu z funkcji, uszkodzimy pamięć - powodując uszkodzenie programu.

Zrozumieć stertę

W przeciwieństwie do stosu sterta jest tym, czego używasz, kiedy chcesz, aby coś istniało przez jakiś czas, niezależnie od funkcji i zakresów. Aby użyć tej pamięci, ‘stdlib’ z języka C jest naprawdę dobrą rzeczą, ponieważ daje dwie niesamowite funkcje: malloc i free.

Malloc (alokacja pamięci) żąda od systemu ilości pamięci, o którą pytano, i zwraca wskaźnik do adresu początkowego. Free mówi systemowi, że pamięć, o którą prosiliśmy, nie jest już potrzebna i może być wykorzystana do innych zadań. Wygląda to naprawdę prosto - tak długo, jak unikasz błędów.

System nie może nadpisać tego, o co prosili deweloperzy. To zależy od nas, ludzi, do zarządzania nim za pomocą dwóch powyższych funkcji. To otwiera drzwi do jednego z ludzkich błędów: Wycieku pamięci.

Wyciek pamięci to pamięć, której żądał użytkownik, a która nigdy nie została zwolniona - kiedy program się zakończył lub zgubiono wskaźniki do jego lokalizacji. To sprawia, że program zużywa znacznie więcej pamięci, niż powinien. Aby tego uniknąć, za każdym razem, gdy nie potrzebujemy już elementu przydzielonego sterty, uwalniamy go.

Wskaźniki: bad i good

Na powyższym obrazie, bad nigdy nie uwalnia pamięci, której używaliśmy. To kończy się marnowaniem 20 * 4 bajtów (rozmiar int w 64-bitach) = 80 bajtów. To może nie wyglądać bardzo poważnie, ale wyobraź sobie, że dzieje się tak w gigantycznym programie. Możemy skończyć, marnując gigabajty!

Zarządzanie pamięcią sterty jest niezbędne, aby zwiększyć wydajność pamięci programów. Ale musisz też uważać, jak go używasz. Podobnie jak w pamięci stosu, po zwolnieniu pamięci, dostęp do niej lub jej użycie może spowodować segfault.

Bonus: Struktury i stos

Jednym z najczęstszych błędów podczas używania struktur jest po prostu zwolnienie struktury. To jest w porządku, o ile nie przydzielamy pamięci do wskaźników wewnątrz struktury. Jeśli pamięć jest przypisana do wskaźników wewnątrz struktury, najpierw musimy ją zwolnić. Wtedy dopiero możemy zwolnić całą strukturę.

Spójrz, jak używam free

Rozwiązywanie problemów z wyciekiem pamięci

Przez większość czasu, kiedy programuję w C, używam struktur. Dlatego zawsze mam dwie obowiązkowe funkcje do użycia z moimi strukturami: constructor i destructor.

Te dwie funkcje są jedynymi, w których używam malloc i free w strukturze. To sprawia, że bardzo proste do rozwiązania są wycieki pamięci.

Sposób na stworzenie i sposób na zniszczenie.

Doskonałe narzędzie do zarządzania pamięcią - Valgrind

Trudno zarządzać pamięcią i być pewnym, że wszystko wykonujesz poprawnie. Doskonałym narzędziem do sprawdzania, czy Twój program zachowuje się poprawnie, jest Valgrind. To narzędzie sprawdza poprawność programu, mówiąc, ile pamięci zostało przydzielone, ile zostało uwolnione oraz kiedy próbujesz pisać w niewłaściwym miejscu w pamięci. Używanie go jest świetnym sposobem sprawdzenia, czy wszystko jest w porządku. Warto go używać, aby uniknąć kompromisów związanych z bezpieczeństwem.

Przykład użycia Valgrind, dający informację o tym, co poszło nie tak


Nie zapomnij mnie obserować!

Poza publikowaniem tutaj, jestem również na Twitterze. Jeśli masz jakieś pytania lub sugestie, nie wahaj się ze mną skontaktować.

<p>Loading...</p>