Kiedy komputer przechowuje zmienną, kiedy program musi uzyskać wartość zmiennej, skąd komputer wie, gdzie szukać w pamięci wartości tej zmiennej?
Kiedy komputer przechowuje zmienną, kiedy program musi uzyskać wartość zmiennej, skąd komputer wie, gdzie szukać w pamięci wartości tej zmiennej?
Odpowiedzi:
Proponuję zajrzeć do cudownego świata konstrukcji kompilatora! Odpowiedź jest taka, że jest to trochę skomplikowany proces.
Aby dać ci intuicję, pamiętaj, że nazwy zmiennych są dostępne wyłącznie dla programisty. Komputer ostatecznie zamieni wszystko w adresy na końcu.
Zmienne lokalne są (ogólnie) przechowywane na stosie: to znaczy są częścią struktury danych, która reprezentuje wywołanie funkcji. Możemy określić pełną listę zmiennych, które funkcja będzie (może) używać, patrząc na tę funkcję, aby kompilator mógł zobaczyć, ile zmiennych potrzebuje dla tej funkcji i ile miejsca zajmuje każda zmienna.
Jest trochę magii zwanej wskaźnikiem stosu, który jest rejestrem, który zawsze przechowuje adres, od którego zaczyna się bieżący stos.
Każda zmienna ma „przesunięcie stosu”, czyli miejsce w stosie, w którym jest przechowywana. Następnie, gdy program musi uzyskać dostęp do zmiennej x
, kompilator zamienia x
się na STACK_POINTER + x_offset
, aby uzyskać rzeczywiste fizyczne miejsce, które jest przechowywane w pamięci.
Zauważ, że dlatego otrzymujesz wskaźnik z powrotem, gdy używasz malloc
lub new
w C lub C ++. Nie możesz ustalić, gdzie dokładnie w pamięci znajduje się wartość przydzielona przez stertę, więc musisz zachować do niej wskaźnik. Ten wskaźnik będzie na stosie, ale będzie wskazywał na stos.
Szczegóły aktualizacji stosów wywołań funkcji i zwrotów są skomplikowane, dlatego polecam The Dragon Book lub The Tiger Book, jeśli jesteś zainteresowany.
Kiedy komputer przechowuje zmienną, kiedy program musi uzyskać wartość zmiennej, skąd komputer wie, gdzie szukać w pamięci wartości tej zmiennej?
Program mówi. Komputery natywnie nie mają pojęcia „zmiennych” - jest to całkowicie język wysokiego poziomu!
Oto program w C:
int main(void)
{
int a = 1;
return a + 3;
}
a oto kod asemblera, który kompiluje: (komentarze zaczynające się od ;
)
main:
; {
pushq %rbp
movq %rsp, %rbp
; int a = 1
movl $1, -4(%rbp)
; return a + 3
movl -4(%rbp), %eax
addl $3, %eax
; }
popq %rbp
ret
Dla „int a = 1;” CPU widzi instrukcję „zapisz wartość 1 pod adresem (wartość rejestru rbp, minus 4)”. Wie, gdzie przechowywać wartość 1, ponieważ program ją mówi.
Podobnie, następna instrukcja mówi „załaduj wartość pod adresem (wartość rejestru rbp, minus 4) do rejestru eax”. Komputer nie musi wiedzieć o takich rzeczach jak zmienne.
%rsp
jest wskaźnikiem stosu procesora. %rbp
jest rejestrem, który odnosi się do bitu stosu używanego przez bieżącą funkcję. Korzystanie z dwóch rejestrów upraszcza debugowanie.
Kiedy kompilator lub interpreter napotka deklarację zmiennej, decyduje, jakiego adresu użyje do przechowywania tej zmiennej, a następnie zapisze adres w tablicy symboli. Po napotkaniu kolejnych odwołań do tej zmiennej adres z tabeli symboli jest zastępowany.
Adres zapisany w tablicy symboli może być przesunięciem względem rejestru (takiego jak wskaźnik stosu), ale jest to szczegół implementacji.
Dokładne metody zależą od tego, o czym konkretnie mówisz i od tego, jak głęboko chcesz zejść. Na przykład przechowywanie plików na dysku twardym różni się od przechowywania czegoś w pamięci lub przechowywania czegoś w bazie danych. Chociaż koncepcje są podobne. A jak to robisz na poziomie programowania, to inne wytłumaczenie niż to, jak komputer robi to na poziomie I / O.
Większość systemów wykorzystuje mechanizmy katalogów / indeksów / rejestrów, aby umożliwić komputerowi znalezienie i dostęp do danych. Ten indeks / katalog będzie zawierał jeden lub więcej kluczy, a adres, w którym faktycznie się znajdują (czy to dysk twardy, pamięć RAM, baza danych itp.).
Przykład programu komputerowego
Program komputerowy może uzyskiwać dostęp do pamięci na różne sposoby. Zazwyczaj system operacyjny nadaje programowi przestrzeń adresową, a program może robić, co chce z tą przestrzenią adresową. Może pisać bezpośrednio na dowolny adres w swojej przestrzeni pamięci i może śledzić, jak chce. Czasami będzie się to różnić w zależności od języka programowania i systemu operacyjnego, a nawet w zależności od preferowanych technik programisty.
Jak wspomniano w niektórych innych odpowiedziach, dokładne stosowane kodowanie lub programowanie jest różne, ale zwykle za kulisami używa czegoś takiego jak stos. Ma rejestr, który przechowuje lokalizację pamięci, w której zaczyna się bieżący stos, a następnie metodę określania, gdzie w tym stosie znajduje się funkcja lub zmienna.
W wielu językach programowania wyższego poziomu zajmuje się tym wszystkim. Wszystko, co musisz zrobić, to zadeklarować zmienną i zapisać coś w tej zmiennej, a to stworzy dla ciebie niezbędne stosy i tablice.
Ale biorąc pod uwagę wszechstronność programowania, tak naprawdę nie ma jednej odpowiedzi, ponieważ programista może w dowolnym momencie napisać bezpośrednio pod dowolny adres w przydzielonej przestrzeni (zakładając, że używa języka programowania, który na to pozwala). Następnie mógłby zapisać jego lokalizację w tablicy, a nawet po prostu zaprogramować go w programie (tj. Zmienna „alfa” jest zawsze zapisywana na początku stosu lub zawsze przechowywana w pierwszych 32 bitach przydzielonej pamięci).
Podsumowanie
Zasadniczo więc za kulisami musi znajdować się mechanizm informujący komputer, gdzie przechowywane są dane. Jednym z najbardziej popularnych sposobów jest jakiś indeks / katalog zawierający klucze i adres pamięci. Jest to realizowane na wiele sposobów i zwykle jest hermetyzowane od użytkownika (a czasem nawet enkapsulowane od programisty).
Odniesienie: Jak komputery pamiętają, gdzie przechowują rzeczy?
Wie o tym dzięki szablonom i formatom.
Program / funkcja / komputer tak naprawdę nie wie, gdzie coś jest. Po prostu oczekuje, że coś znajdzie się w określonym miejscu. Użyjmy przykładu.
class simpleClass{
public:
int varA=58;
int varB=73;
simpleClass* nextObject=NULL;
};
Nasza nowa klasa „simpleClass” zawiera 3 ważne zmienne - dwie liczby całkowite, które mogą zawierać pewne dane, kiedy są potrzebne, oraz wskaźnik do innego „obiektu simpleClass”. Załóżmy, że jesteśmy na komputerze 32-bitowym dla uproszczenia. „gcc” lub inny kompilator „C” stworzyłby dla nas szablon do pracy przy alokacji niektórych danych.
Proste typy
Po pierwsze, gdy używa się słowa kluczowego dla prostego typu, takiego jak „int”, kompilator zapisuje notatkę w sekcji „.data” lub „.bss” pliku wykonywalnego, aby dane były wykonywane przez system operacyjny dostępne dla programu. Słowo kluczowe „int” przydzieli 4 bajty (32 bity), a „długie int” przydzieli 8 bajtów (64 bity).
Czasami, w komórce po komórce, zmienna może przyjść zaraz po instrukcji, która ma ją załadować do pamięci, więc wyglądałoby to tak w pseudo-montażu:
...
clear register EAX
clear register EBX
load the immediate (next) value into EAX
5
copy the value in register EAX to register EBX
...
Skończyłoby się to wartością „5” w EAX oraz EBX.
Podczas wykonywania programu wykonywana jest każda instrukcja z wyjątkiem „5” od momentu bezpośredniego załadowania odnosi się do niej i powoduje, że procesor przeskakuje nad nią.
Minusem tej metody jest to, że jest ona naprawdę praktyczna tylko dla stałych, ponieważ nie byłoby praktyczne utrzymywanie tablic / buforów / ciągów w środku kodu. Ogólnie więc większość zmiennych jest przechowywana w nagłówkach programu.
Gdyby trzeba było uzyskać dostęp do jednej z tych zmiennych dynamicznych, można traktować wartość natychmiastową tak, jakby była wskaźnikiem:
...
clear register EAX
clear register EBX
load the immediate value into EAX
0x0AF2CE66 (Let's say this is the address of a cell containing '5')
load the value pointed to by EAX into EBX
...
Kończyłoby się to wartością „0x0AF2CE66” w rejestrze EAX i wartością „5” w rejestrze EBX. Można również dodawać wartości do rejestrów razem, abyśmy byli w stanie znaleźć elementy tablicy lub łańcucha przy użyciu tej metody.
Inną ważną kwestią jest to, że można przechowywać wartości, używając adresów w podobny sposób, aby móc później odwoływać się do wartości w tych komórkach.
Typy złożone
Jeśli wykonamy dwa obiekty tej klasy:
simpleClass newObjA;
simpleClass newObjB;
następnie możemy przypisać wskaźnik do drugiego obiektu do pola dostępnego dla niego w pierwszym obiekcie:
newObjA.nextObject=&newObjB;
Teraz program może oczekiwać adresu drugiego obiektu w polu wskaźnika pierwszego obiektu. W pamięci wyglądałoby to tak:
newObjA: 58
73
&newObjB
...
newObjB: 58
73
NULL
Jednym bardzo ważnym faktem, o którym należy tutaj wspomnieć, jest to, że „newObjA” i „newObjB” nie mają nazw podczas kompilacji. To tylko miejsca, w których spodziewamy się pewnych danych. Tak więc, jeśli dodamy 2 komórki do & newObjA, wówczas znajdziemy komórkę, która działa jako „nextObject”. Dlatego jeśli znamy adres „newObjA” i gdzie komórka „nextObject” jest względem niego powiązana, możemy poznać adres „newObjB”:
...
load the immediate value into EAX
&newObjA
add the immediate value to EAX
2
load the value in EAX into EBX
Skończy się to na „2 + i newObjA” w „EAX” oraz „& newObjB” w „EBX”.
Szablony / formaty
Kiedy kompilator kompiluje definicję klasy, naprawdę kompiluje sposób tworzenia formatu, sposób zapisu do formatu oraz sposób odczytu z formatu.
Powyższy przykład to szablon pojedynczo połączonej listy z dwiema zmiennymi „int”. Tego rodzaju konstrukcje są bardzo ważne dla dynamicznej alokacji pamięci, podobnie jak drzewa binarne i n-ary. Praktycznymi zastosowaniami drzew n-ary byłyby systemy plików złożone z katalogów wskazujących pliki, katalogi lub inne instancje rozpoznawane przez sterowniki / system operacyjny.
Aby uzyskać dostęp do wszystkich elementów, zastanów się nad robakiem calowym poruszającym się w górę i w dół struktury. W ten sposób program / funkcja / komputer nic nie wie, po prostu wykonuje instrukcje przenoszenia danych.