Jak komputery pamiętają, gdzie przechowują rzeczy?


32

Kiedy komputer przechowuje zmienną, kiedy program musi uzyskać wartość zmiennej, skąd komputer wie, gdzie szukać w pamięci wartości tej zmiennej?


17
Nie; „komputer” jest całkowicie nieświadomy. Musimy zakodować wszystkie adresy. (Co nieco upraszcza, ale nie za bardzo.)
Raphael

1
@Raphael: Uogólnijmy to na „musimy zakodować adresy podstawowe”.
fresnel

Za każdym razem, gdy deklarujesz zmienną, program odpowiedzialny za uruchomienie twojego kodu dołącza nazwę zmiennej wraz z jej adresem do tablicy hasht (czyli przestrzeni nazw). Sugerowałbym przeczytanie książki „Struktura i wdrażanie programów komputerowych (SICP), aby dobrze poznać takie drobne szczegóły.
Abhirath Mahipal,

Twój program źródłowy używa zmiennej. Kompilator lub interpreter decyduje o tym, jak go zaimplementować: generuje instrukcje do wykonania przez komputer i musi się upewnić, że te intruzi pobierają wartości z miejsc, w których były przechowywane poprzednie instrukcje.
PJTraill

1
@AbhirathMahipal: zmienna nie musi mieć adresu w czasie kompilacji ani nawet w czasie wykonywania; „Przestrzeń nazw” to koncepcja języka, podczas gdy tabela (mieszana lub inna) jest szczegółem implementacji; nazwa musi pozostać w programie, gdy jest uruchomiony.
PJTraill,

Odpowiedzi:


31

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 xsię 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 malloclub neww 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.


24

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.


2
Aby połączyć to z odpowiedzią jmite, %rspjest wskaźnikiem stosu procesora. %rbpjest rejestrem, który odnosi się do bitu stosu używanego przez bieżącą funkcję. Korzystanie z dwóch rejestrów upraszcza debugowanie.
MSalters

2

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.


0

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?


0

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.


Użyte tutaj słowa „szablon” i „format” nie pojawiają się w żadnym kompilatorze lub podręczniku kompilatora, jaki kiedykolwiek widziałem, i nie wydaje się, aby istniał żaden powód, aby używać obu słów dla tej samej nieistniejącej rzeczy. Zmienne mają adresy i / lub przesunięcia, to wszystko, co musisz wiedzieć.
user207421,

Używam tych słów, ponieważ są to abstrakcje do uporządkowania danych, podobnie jak liczby, pliki, tablice i zmienne są abstrakcjami.
Pan Minty Fresh
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.