W językach programowania, takich jak C i C ++, ludzie często odnoszą się do statycznej i dynamicznej alokacji pamięci. Rozumiem tę koncepcję, ale fraza „Cała pamięć została przydzielona (zarezerwowana) w czasie kompilacji” zawsze mnie dezorientuje.
Kompilacja, jak rozumiem, konwertuje kod C / C ++ wysokiego poziomu na język maszynowy i generuje plik wykonywalny. W jaki sposób przydzielana jest pamięć w skompilowanym pliku? Czy pamięć nie jest zawsze przydzielana w pamięci RAM ze wszystkimi elementami zarządzania pamięcią wirtualną?
Czy alokacja pamięci nie jest z definicji pojęciem środowiska uruchomieniowego?
Jeśli w moim kodzie C / C ++ utworzę statycznie przydzieloną zmienną 1 KB, czy spowoduje to zwiększenie rozmiaru pliku wykonywalnego o tę samą wartość?
To jest jedna ze stron, na której fraza jest używana pod nagłówkiem „Alokacja statyczna”.
źródło
Odpowiedzi:
Pamięć przydzielona w czasie kompilacji oznacza, że kompilator rozwiązuje problem w czasie kompilacji, w którym pewne rzeczy zostaną przydzielone w mapie pamięci procesu.
Na przykład rozważmy tablicę globalną:
Kompilator wie w czasie kompilacji rozmiar tablicy i rozmiar an
int
, więc zna cały rozmiar tablicy w czasie kompilacji. Również zmienna globalna ma domyślnie statyczny czas trwania: jest alokowana w obszarze pamięci statycznej obszaru pamięci procesu (sekcja .data / .bss). Biorąc pod uwagę te informacje, kompilator decyduje podczas kompilacji, w jakim adresie tego statycznego obszaru pamięci będzie tablica .Oczywiście te adresy pamięci są adresami wirtualnymi. Program zakłada, że ma własną całą przestrzeń pamięci (na przykład od 0x00000000 do 0xFFFFFFFF). Dlatego kompilator mógł wykonać założenia typu „OK, tablica będzie miała adres 0x00A33211”. W czasie wykonywania te adresy są tłumaczone na adresy rzeczywiste / sprzętowe przez MMU i system operacyjny.
Wartość zainicjowanej wartości statycznej pamięci masowej jest nieco inna. Na przykład:
W naszym pierwszym przykładzie kompilator zdecydował tylko, gdzie tablica zostanie przydzielona, przechowując te informacje w pliku wykonywalnym.
W przypadku rzeczy zainicjowanych wartością, kompilator również wstrzykuje wartość początkową tablicy do pliku wykonywalnego i dodaje kod, który mówi programowi ładującemu, że po przydzieleniu tablicy przy uruchomieniu programu tablica powinna zostać wypełniona tymi wartościami.
Oto dwa przykłady zestawu wygenerowanego przez kompilator (GCC4.8.1 z celem x86):
Kod C ++:
Zespół wyjściowy:
Jak widać, wartości są wprowadzane bezpośrednio do zespołu. W tablicy
a
kompilator generuje zerową inicjalizację 16 bajtów, ponieważ Standard mówi, że statyczne przechowywane rzeczy powinny być domyślnie inicjowane do zera:Zawsze sugeruję ludziom rozmontowanie kodu, aby zobaczyć, co kompilator naprawdę robi z kodem C ++. Dotyczy to klas pamięci / czasu trwania (jak to pytanie) do zaawansowanych optymalizacji kompilatora. Możesz poinstruować kompilator, aby wygenerował asemblację, ale w Internecie są wspaniałe narzędzia do tego w przyjazny sposób. Moim ulubionym jest GCC Explorer .
źródło
Pamięć przydzielona w czasie kompilacji oznacza po prostu, że nie będzie dalszej alokacji w czasie wykonywania - brak wywołań do malloc, new lub innych metod alokacji dynamicznej. Będziesz mieć stałą ilość użycia pamięci, nawet jeśli nie potrzebujesz całej tej pamięci przez cały czas.
Pamięć nie jest używana przed uruchomieniem, ale bezpośrednio przed uruchomieniem jej alokacja jest obsługiwana przez system.
Samo zadeklarowanie wartości statycznej nie zwiększy rozmiaru pliku wykonywalnego o więcej niż kilka bajtów. Zadeklarowanie wartości początkowej niezerowej spowoduje (w celu utrzymania tej wartości początkowej). Zamiast tego konsolidator po prostu dodaje tę kwotę 1 KB do wymagań dotyczących pamięci, które program ładujący systemu tworzy dla Ciebie bezpośrednio przed wykonaniem.
źródło
static int i[4] = {2 , 3 , 5 ,5 }
, zwiększy się o rozmiar pliku wykonywalnego o 16 bajtów. Powiedziałeś: „Po prostu zadeklarowanie wartości statycznej nie zwiększy rozmiaru pliku wykonywalnego o więcej niż kilka bajtów. Zadeklarowanie go z wartością początkową niezerową spowoduje”. Zadeklarowanie wartości początkowej spowoduje, co to oznacza.Pamięć przydzielona w czasie kompilacji oznacza, że po załadowaniu programu pewna część pamięci zostanie natychmiast przydzielona, a rozmiar i (względna) pozycja tej alokacji jest określana w czasie kompilacji.
Te 3 zmienne są „przydzielane w czasie kompilacji”, co oznacza, że kompilator oblicza ich rozmiar (który jest ustalony) w czasie kompilacji. Zmienna
a
będzie przesunięciem w pamięci, powiedzmy, wskazującym na adres 0,b
będzie wskazywać na adres 33 ic
34 (zakładając, że nie ma optymalizacji wyrównania). Zatem przydzielenie 1Kb danych statycznych nie zwiększy rozmiaru twojego kodu , ponieważ zmieni tylko przesunięcie w nim. Rzeczywista przestrzeń zostanie przydzielona w czasie ładowania .Prawdziwa alokacja pamięci zawsze odbywa się w czasie wykonywania, ponieważ jądro musi to śledzić i aktualizować swoje wewnętrzne struktury danych (ile pamięci jest przydzielane dla każdego procesu, stron i tak dalej). Różnica polega na tym, że kompilator już zna rozmiar wszystkich danych, których zamierzasz użyć, i jest on przydzielany zaraz po wykonaniu programu.
Pamiętaj też, że mówimy o adresach względnych . Rzeczywisty adres, pod którym będzie się znajdować zmienna, będzie inny. W czasie ładowania jądro zarezerwuje trochę pamięci dla procesu, powiedzmy pod adresem
x
, a wszystkie zakodowane na stałe adresy zawarte w pliku wykonywalnym zostaną zwiększone ox
bajty, tak że zmiennaa
w przykładzie będzie pod adresemx
, b pod adresemx+33
i wkrótce.źródło
Dodanie zmiennych na stosie, które zajmują N bajtów, nie (koniecznie) nie zwiększa rozmiaru pojemnika o N bajtów. W rzeczywistości przez większość czasu będzie dodawać tylko kilka bajtów.
Zacznijmy od przykładu jak dodanie 1000 znaków w kodzie woli zwiększyć rozmiar kosza w sposób liniowy.
Jeśli 1k jest ciągiem tysiąca znaków, to jest tak zadeklarowane
i wtedy
vim your_compiled_bin
miałbyś, faktycznie byłbyś w stanie zobaczyć ten ciąg gdzieś w koszu. W takim przypadku tak: plik wykonywalny będzie większy o 1 k, ponieważ zawiera cały ciąg.Jeśli jednak przydzielisz tablicę
int
s,char
s lublong
s na stosie i przypiszesz ją w pętli, coś wzdłuż tych liniiwtedy nie: nie zwiększy kosza ... przez
1000*sizeof(int)
Alokację w czasie kompilacji oznacza to, co teraz zrozumiałeś to oznacza (na podstawie twoich komentarzy): skompilowany kosz zawiera informacje, których system potrzebuje, aby wiedzieć, ile pamięci jaka funkcja / blok będzie potrzebna, gdy zostanie wykonana, wraz z informacją o rozmiarze stosu, którego wymaga Twoja aplikacja. To jest to, co system przydzieli, kiedy wykona twój bin, a twój program stanie się procesem (cóż, wykonanie twojego bin to proces, który ... cóż, rozumiesz, o czym mówię).
Oczywiście nie maluję tutaj całego obrazu: Kosz zawiera informacje o tym, jak duży stos będzie faktycznie potrzebny. Na podstawie tych informacji (między innymi), system zarezerwuje fragment pamięci, zwany stosem, nad którym program będzie mógł swobodnie rządzić. Pamięć stosu nadal jest przydzielana przez system, gdy proces (wynik wykonania binarki) jest inicjowany. Następnie proces zarządza pamięcią stosu za Ciebie. Kiedy funkcja lub pętla (dowolny typ bloku) jest wywoływana / wykonywana, zmienne lokalne dla tego bloku są umieszczane na stosie i są usuwane (pamięć stosu jest „zwalniana”, że tak powiem) do wykorzystania przez inne funkcje / bloki. Tak deklaruję
int some_array[100]
doda tylko kilka bajtów dodatkowych informacji do kosza, co powie systemowi, że funkcja X będzie wymagać100*sizeof(int)
+ dodatkowa przestrzeń księgowa.źródło
i
nie jest „zwalniana” ani też. Gdybyi
rezydował w pamięci, po prostu zostałby zepchnięty na stos, coś, co nie jest uwolnione w tym znaczeniu tego słowa, pomijając toi
lubc
będzie przechowywane w rejestrach przez cały czas. Oczywiście wszystko zależy od kompilatora, co oznacza, że nie jest tak czarno-biały.free()
wywołań, ale pamięć stosu, której używali, jest wolna do wykorzystania przez inne funkcje, gdy funkcja, którą wymieniłem, powróci.Na wielu platformach wszystkie globalne lub statyczne alokacje w każdym module zostaną skonsolidowane przez kompilator w trzy lub mniej skonsolidowanych przydziałów (jeden dla niezainicjowanych danych (często nazywany „bss”), drugi dla zainicjowanych danych do zapisu (często nazywanych „danymi” ) i jeden dla stałych danych („const”)), a wszystkie globalne lub statyczne alokacje każdego typu w programie zostaną skonsolidowane przez linker w jeden globalny dla każdego typu. Na przykład, zakładając
int
cztery bajty, moduł ma następujące statyczne alokacje:powiedziałby linkerowi, że potrzebuje 208 bajtów na bss, 16 bajtów na "dane" i 28 bajtów na "const". Co więcej, każde odniesienie do zmiennej zostanie zastąpione selektorem obszaru i przesunięciem, więc a, b, c, d i e zostaną zastąpione przez bss + 0, const + 0, bss + 4, const + 24, data +0 lub bss + 204, odpowiednio.
Kiedy program jest połączony, wszystkie obszary bss ze wszystkich modułów są łączone razem; podobnie jak obszary danych i const. Dla każdego modułu adres wszelkich zmiennych odnoszących się do bss zostanie zwiększony o rozmiar obszarów bss wszystkich poprzedzających modułów (ponownie, podobnie z danymi i const). Tak więc, kiedy linker zostanie ukończony, każdy program będzie miał jedną alokację bss, jedną alokację danych i jedną alokację const.
Kiedy program jest ładowany, w zależności od platformy zwykle dzieje się jedna z czterech rzeczy:
Plik wykonywalny wskaże, ile bajtów potrzebuje dla każdego rodzaju danych oraz - dla zainicjowanego obszaru danych, w którym można znaleźć początkową zawartość. Będzie również zawierać listę wszystkich instrukcji, które używają adresu względnego bss, data lub const. System operacyjny lub program ładujący przydzieli odpowiednią ilość miejsca dla każdego obszaru, a następnie doda adres początkowy tego obszaru do każdej instrukcji, która tego potrzebuje.
System operacyjny przydzieli porcję pamięci do przechowywania wszystkich trzech rodzajów danych i poda aplikacji wskaźnik do tej porcji pamięci. Każdy kod, który używa danych statycznych lub globalnych, wyłuskuje je względem tego wskaźnika (w wielu przypadkach wskaźnik będzie przechowywany w rejestrze przez cały okres istnienia aplikacji).
System operacyjny początkowo nie przydzieli żadnej pamięci do aplikacji, z wyjątkiem tego, co przechowuje jej kod binarny, ale pierwszą rzeczą, którą aplikacja zrobi, będzie zażądanie odpowiedniego przydziału z systemu operacyjnego, który na zawsze będzie przechowywać w rejestrze.
System operacyjny początkowo nie przydzieli miejsca dla aplikacji, ale aplikacja zażąda odpowiedniego przydziału przy uruchomieniu (jak wyżej). Aplikacja będzie zawierała listę instrukcji z adresami, które należy zaktualizować, aby odzwierciedlić miejsce przydzielenia pamięci (jak w przypadku pierwszego stylu), ale zamiast łatania aplikacji przez moduł ładujący system operacyjny, aplikacja będzie zawierała wystarczającą ilość kodu, aby załatać samą siebie .
Wszystkie cztery podejścia mają zalety i wady. Jednak w każdym przypadku kompilator skonsoliduje dowolną liczbę zmiennych statycznych w ustaloną niewielką liczbę żądań pamięci, a konsolidator skonsoliduje je wszystkie w niewielką liczbę skonsolidowanych alokacji. Mimo że aplikacja będzie musiała otrzymać porcję pamięci z systemu operacyjnego lub programu ładującego, to kompilator i konsolidator są odpowiedzialne za przydzielanie poszczególnych elementów z tej dużej porcji do wszystkich indywidualnych zmiennych, które jej potrzebują.
źródło
Rdzeń twojego pytania jest następujący: „W jaki sposób pamięć jest„ alokowana ”w skompilowanym pliku? Czy pamięć nie jest zawsze przydzielana w pamięci RAM wraz z całym zarządzaniem pamięcią wirtualną? Czy alokacja pamięci nie jest z definicji pojęciem środowiska wykonawczego?”
Myślę, że problem polega na tym, że istnieją dwie różne koncepcje związane z alokacją pamięci. Uogólniając, alokacja pamięci jest procesem, w którym mówimy „ta pozycja danych jest przechowywana w tej konkretnej części pamięci”. W nowoczesnym systemie komputerowym obejmuje to dwuetapowy proces:
Ten ostatni proces jest czysto wykonywany w czasie wykonywania, ale pierwszy można wykonać w czasie kompilacji, jeśli dane mają znany rozmiar i wymagana jest ich stała liczba. Oto w zasadzie, jak to działa:
Kompilator widzi plik źródłowy zawierający linię, która wygląda trochę tak:
Tworzy dane wyjściowe dla asemblera, który instruuje go, aby zarezerwował pamięć dla zmiennej „c”. Może to wyglądać tak:
Kiedy asembler działa, utrzymuje licznik, który śledzi przesunięcia każdego elementu od początku „segmentu” pamięci (lub „sekcji”). Jest to podobne do części bardzo dużej „struktury”, która zawiera wszystko w całym pliku, ale nie ma obecnie przydzielonej żadnej pamięci i może znajdować się gdziekolwiek. Notuje w tabeli, która
_c
ma określone przesunięcie (powiedzmy 510 bajtów od początku segmentu), a następnie zwiększa swój licznik o 4, więc następna taka zmienna będzie miała (np.) 514 bajtów. Dla każdego kodu, który potrzebuje adresu_c
, po prostu umieszcza 510 w pliku wyjściowym i dodaje uwagę, że wyjście wymaga adresu segmentu, który zawiera_c
dodanie do niego później.Konsolidator pobiera wszystkie pliki wyjściowe asemblera i bada je. Określa adres dla każdego segmentu, tak aby się nie nakładały, i dodaje niezbędne przesunięcia, aby instrukcje nadal odnosiły się do właściwych elementów danych. W przypadku niezainicjowanej pamięci, takiej jak ta zajmowana przez
c
(asemblerowi powiedziano, że pamięć będzie niezainicjowana przez fakt, że kompilator umieścił ją w segmencie '.bss', który jest nazwą zarezerwowaną dla niezainicjowanej pamięci), zawiera pole nagłówka w swoim wyjściu, które informuje system operacyjny ile trzeba zarezerwować. Może zostać przeniesiony (i zwykle jest), ale zwykle jest zaprojektowany tak, aby był ładowany bardziej efektywnie pod jednym określonym adresem pamięci, a system operacyjny będzie próbował załadować go pod tym adresem. W tym momencie mamy całkiem niezłe pojęcie, przez jaki adres wirtualny będzie używanyc
.Adres fizyczny nie zostanie w rzeczywistości określony, dopóki program nie zostanie uruchomiony. Jednak z punktu widzenia programisty adres fizyczny jest w rzeczywistości nieistotny - nigdy się nawet nie dowiemy, co to jest, ponieważ system operacyjny zwykle nie zawraca sobie głowy informowaniem nikogo, może się często zmieniać (nawet gdy program jest uruchomiony), a i tak głównym celem systemu operacyjnego jest oderwanie tego.
źródło
Plik wykonywalny opisuje, jakie miejsce należy przydzielić na zmienne statyczne. Ta alokacja jest wykonywana przez system, kiedy uruchamiasz plik wykonywalny. Więc twoja statyczna zmienna 1kB nie zwiększy rozmiaru pliku wykonywalnego o 1kB:
O ile oczywiście nie określisz inicjatora:
Tak więc, oprócz „języka maszynowego” (tj. Instrukcji procesora), plik wykonywalny zawiera opis wymaganego układu pamięci.
źródło
Pamięć można przydzielić na wiele sposobów:
Teraz twoje pytanie brzmi, co to jest „pamięć przydzielana w czasie kompilacji”. Zdecydowanie jest to po prostu niepoprawnie sformułowane powiedzenie, które ma odnosić się do alokacji segmentów binarnych lub alokacji stosu, lub w niektórych przypadkach nawet do alokacji sterty, ale w tym przypadku alokacja jest ukryta przed oczami programisty przez niewidzialne wywołanie konstruktora. Lub prawdopodobnie osoba, która powiedziała, że chce tylko powiedzieć, że pamięć nie jest przydzielana na stosie, ale nie wiedziała o alokacji stosu lub segmentu (lub nie chciała wdawać się w tego rodzaju szczegóły).
Ale w większości przypadków osoba chce tylko powiedzieć, że ilość przydzielonej pamięci jest znana w czasie kompilacji .
Rozmiar binarny zmieni się tylko wtedy, gdy pamięć zostanie zarezerwowana w kodzie lub segmencie danych aplikacji.
źródło
.data
i.bss
.Masz rację. Pamięć jest faktycznie przydzielana (stronicowana) w czasie ładowania, tj. Gdy plik wykonywalny jest wprowadzany do (wirtualnej) pamięci. W tym momencie można również zainicjować pamięć. Kompilator po prostu tworzy mapę pamięci. [Nawiasem mówiąc, przestrzenie stosu i sterty są również przydzielane podczas ładowania!]
źródło
Myślę, że musisz się trochę cofnąć. Pamięć przydzielona w czasie kompilacji… Co to może znaczyć? Czy może to oznaczać, że pamięć w chipach, które nie zostały jeszcze wyprodukowane, dla komputerów, które nie zostały jeszcze zaprojektowane, jest w jakiś sposób rezerwowana? Nie, podróże w czasie, żadnych kompilatorów, które mogą manipulować wszechświatem.
Musi więc oznaczać, że kompilator generuje instrukcje, aby w jakiś sposób przydzielić tę pamięć w czasie wykonywania. Ale jeśli spojrzysz na to pod odpowiednim kątem, kompilator generuje wszystkie instrukcje, więc jaka może być różnica. Różnica polega na tym, że decyduje kompilator, aw czasie wykonywania kod nie może zmieniać ani modyfikować swoich decyzji. Jeśli zdecydował, że potrzebuje 50 bajtów w czasie kompilacji, w czasie wykonywania, nie możesz zdecydować o przydzieleniu 60 - ta decyzja została już podjęta.
źródło
Jeśli nauczysz się programowania w asemblerze, zobaczysz, że musisz wyrzeźbić segmenty dla danych, stosu i kodu, itp. Segment danych to miejsce, w którym żyją ciągi i liczby. Segment kodu to miejsce, w którym żyje Twój kod. Segmenty te są wbudowane w program wykonywalny. Oczywiście rozmiar stosu jest również ważny ... nie chciałbyś, aby stos się przepełnił !
Więc jeśli twój segment danych ma 500 bajtów, twój program ma 500 bajtów. Jeśli zmienisz segment danych na 1500 bajtów, rozmiar programu będzie większy o 1000 bajtów. Dane są montowane w rzeczywistym programie.
To właśnie się dzieje, gdy kompilujesz języki wyższego poziomu. Rzeczywisty obszar danych jest przydzielany podczas kompilacji do programu wykonywalnego, co zwiększa rozmiar programu. Program może również żądać pamięci w locie, a to jest pamięć dynamiczna. Możesz zażądać pamięci z pamięci RAM, a procesor da ci ją do użycia, możesz ją puścić, a twój garbage collector zwolni ją z powrotem do CPU. W razie potrzeby można go nawet zamienić na dysk twardy przez dobrego menedżera pamięci. Te funkcje zapewniają języki wysokiego poziomu.
źródło
Chciałbym wyjaśnić te pojęcia za pomocą kilku diagramów.
To prawda, że na pewno pamięci nie można przydzielić w czasie kompilacji. Ale co dzieje się w rzeczywistości w czasie kompilacji.
Oto wyjaśnienie. Powiedzmy na przykład, że program ma cztery zmienne x, y, z i k. Teraz, w czasie kompilacji, po prostu tworzy mapę pamięci, na której ustalane jest położenie tych zmiennych względem siebie. Ten diagram lepiej to zilustruje.
Teraz wyobraź sobie, że żaden program nie działa w pamięci. Pokazuję to dużym pustym prostokątem.
Następnie wykonywane jest pierwsze wystąpienie tego programu. Możesz to wizualizować w następujący sposób. Jest to czas, w którym faktycznie przydzielana jest pamięć.
Gdy uruchomiona jest druga instancja tego programu, pamięć będzie wyglądać następująco.
I trzecia…
Itd. itp.
Mam nadzieję, że ta wizualizacja dobrze wyjaśnia tę koncepcję.
źródło
Przyjęta odpowiedź zawiera bardzo ładne wyjaśnienie. Na wszelki wypadek opublikuję link, który uznałem za przydatny. https://www.tenouk.com/ModuleW.html
źródło