Co się stanie, gdy uruchomi się program komputerowy?


180

Znam ogólną teorię, ale nie mogę dopasować się do szczegółów.

Wiem, że program znajduje się w dodatkowej pamięci komputera. Gdy program rozpocznie wykonywanie, jest całkowicie kopiowany do pamięci RAM. Następnie procesor pobiera jednocześnie kilka instrukcji (w zależności od wielkości magistrali), umieszcza je w rejestrach i wykonuje je.

Wiem również, że program komputerowy wykorzystuje dwa rodzaje pamięci: stos i stertę, które są również częścią podstawowej pamięci komputera. Stos jest używany do pamięci niedynamicznej, a sterty do pamięci dynamicznej (na przykład wszystko związane znew operatorem w C ++)

Nie rozumiem, jak te dwie rzeczy się łączą. W którym momencie stos jest używany do wykonywania instrukcji? Instrukcje idą z pamięci RAM, na stos, do rejestrów?


43
+1 za zadanie podstawowego pytania!
mkelley33

21
hmm ... no wiesz, piszą o tym książki. Czy naprawdę chcesz przestudiować tę część architektury systemu operacyjnego za pomocą SO?
Andrey

1
Dodałem kilka tagów w oparciu o charakter pytania związany z pamięcią i odniesienie do C ++, chociaż myślę, że dobra odpowiedź mogłaby również pochodzić od kogoś znającego się na Javie lub C #!)
mkelley33

14
Głosował i faworyzował. Zawsze bałam się zapytać ...
Maks.

2
Termin „umieszcza je w rejestrach” nie jest do końca właściwy. W większości procesorów rejestry służą do przechowywania wartości pośrednich, a nie kodu wykonywalnego.

Odpowiedzi:


161

To zależy od systemu, ale współczesne systemy operacyjne z pamięcią wirtualną mają tendencję do ładowania obrazów procesów i przydzielania pamięci w taki sposób:

+---------+
|  stack  |  function-local variables, return addresses, return values, etc.
|         |  often grows downward, commonly accessed via "push" and "pop" (but can be
|         |  accessed randomly, as well; disassemble a program to see)
+---------+
| shared  |  mapped shared libraries (C libraries, math libs, etc.)
|  libs   |
+---------+
|  hole   |  unused memory allocated between the heap and stack "chunks", spans the
|         |  difference between your max and min memory, minus the other totals
+---------+
|  heap   |  dynamic, random-access storage, allocated with 'malloc' and the like.
+---------+
|   bss   |  Uninitialized global variables; must be in read-write memory area
+---------+
|  data   |  data segment, for globals and static variables that are initialized
|         |  (can further be split up into read-only and read-write areas, with
|         |  read-only areas being stored elsewhere in ROM on some systems)
+---------+
|  text   |  program code, this is the actual executable code that is running.
+---------+

Jest to ogólna przestrzeń adresowa procesu w wielu popularnych systemach pamięci wirtualnej. „Dziura” jest rozmiarem całkowitej pamięci, pomniejszonym o przestrzeń zajmowaną przez wszystkie pozostałe obszary; daje to dużą ilość miejsca na powiększenie sterty. Jest to również „wirtualne”, co oznacza, że ​​mapuje na twoją rzeczywistą pamięć za pomocą tabeli translacji i może być faktycznie przechowywane w dowolnym miejscu w rzeczywistej pamięci. Odbywa się to w ten sposób, aby zabezpieczyć jeden proces przed dostępem do pamięci innego procesu i sprawić, aby każdy proces myślał, że działa na pełnym systemie.

Zauważ, że pozycje np. Stosu i sterty mogą być w innej kolejności w niektórych systemach (patrz odpowiedź Billy'ego O'Neala) więcej informacji na temat Win32 można poniżej).

Inne systemy mogą się bardzo różnić. Na przykład DOS działał w trybie rzeczywistym , a przydzielanie pamięci podczas uruchamiania programów wyglądało zupełnie inaczej:

+-----------+ top of memory
| extended  | above the high memory area, and up to your total memory; needed drivers to
|           | be able to access it.
+-----------+ 0x110000
|  high     | just over 1MB->1MB+64KB, used by 286s and above.
+-----------+ 0x100000
|  upper    | upper memory area, from 640kb->1MB, had mapped memory for video devices, the
|           | DOS "transient" area, etc. some was often free, and could be used for drivers
+-----------+ 0xA0000
| USER PROC | user process address space, from the end of DOS up to 640KB
+-----------+
|command.com| DOS command interpreter
+-----------+ 
|    DOS    | DOS permanent area, kept as small as possible, provided routines for display,
|  kernel   | *basic* hardware access, etc.
+-----------+ 0x600
| BIOS data | BIOS data area, contained simple hardware descriptions, etc.
+-----------+ 0x400
| interrupt | the interrupt vector table, starting from 0 and going to 1k, contained 
|  vector   | the addresses of routines called when interrupts occurred.  e.g.
|  table    | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that 
|           | location to service the interrupt.
+-----------+ 0x0

Widać, że DOS zezwalał na bezpośredni dostęp do pamięci systemu operacyjnego, bez żadnej ochrony, co oznaczało, że programy w przestrzeni użytkownika mogły zasadniczo bezpośrednio uzyskiwać dostęp lub zastępować wszystko, co im się podoba.

Jednak w przestrzeni adresowej procesu programy wyglądały podobnie, tyle że były one opisywane jako segment kodu, segment danych, sterta, segment stosu itp. I było nieco inaczej odwzorowane. Ale większość ogólnych obszarów wciąż tam była.

Po załadowaniu programu i niezbędnych współdzielonych bibliotek do pamięci oraz rozprowadzeniu części programu do odpowiednich obszarów, system operacyjny zaczyna wykonywać proces wszędzie tam, gdzie jest jego główna metoda, a program przejmuje stamtąd, wykonując wywołania systemowe w razie potrzeby, gdy potrzebuje ich.

Różne systemy (osadzone, cokolwiek) mogą mieć bardzo różne architektury, takie jak systemy bez stosów, systemy architektury Harvard (z kodem i danymi przechowywanymi w osobnej pamięci fizycznej), systemy, które faktycznie utrzymują BSS w pamięci tylko do odczytu (początkowo ustawione przez programista) itp. Ale to jest ogólna istota.


Powiedziałeś:

Wiem również, że program komputerowy wykorzystuje dwa rodzaje pamięci: stos i stertę, które są również częścią podstawowej pamięci komputera.

„Stos” i „stos” są po prostu abstrakcyjnymi pojęciami, a nie (koniecznie) fizycznie odrębnymi „rodzajami” pamięci.

Stos jest jedynie last in, first out struktura danych. W architekturze x86 można ją rozwiązać losowo, używając przesunięcia względem końca, ale najczęściej używanymi funkcjami są PUSH i POP, odpowiednio, do dodawania i usuwania elementów. Jest powszechnie używany do zmiennych lokalnych funkcji (tak zwane „automatyczne przechowywanie”), argumentów funkcji, adresów zwrotnych itp. (Więcej poniżej)

„Kupa” to tylko pseudonim na fragmencie pamięci, która może zostać przydzielona na żądanie, i skierowana jest losowy (co oznacza, można uzyskać dostęp do dowolnej lokalizacji w nim bezpośrednio). Jest powszechnie używany w strukturach danych, które alokujesz w czasie wykonywania (w C ++, używając newi delete, imalloc i przyjaciele w C itp.).

Stos i sterta w architekturze x86 znajdują się fizycznie w pamięci systemowej (RAM) i są mapowane poprzez alokację pamięci wirtualnej do przestrzeni adresowej procesu, jak opisano powyżej.

Te rejestry (wciąż x86) fizycznie przebywania wewnątrz procesora (w przeciwieństwie do pamięci RAM) i ładowane są przez procesor, z obszaru TEXT (i może być również ładowany z zewnątrz w pamięci lub w innych miejscach, w zależności od instrukcji procesorowych są faktycznie wykonywane). Są to po prostu bardzo małe, bardzo szybkie lokalizacje pamięci na chipie, które są wykorzystywane do wielu różnych celów.

Układ rejestru jest w dużym stopniu zależny od architektury (w rzeczywistości rejestry, zestaw instrukcji i układ / projekt pamięci są dokładnie tym, co rozumie się przez „architekturę”), więc nie będę się na nim rozwijać, ale zalecam wziąć kurs języka asemblera, aby lepiej je zrozumieć.


Twoje pytanie:

W którym momencie stos jest używany do wykonywania instrukcji? Instrukcje idą z pamięci RAM, na stos, do rejestrów?

Stos (w systemach / językach, które je mają i używają) jest najczęściej używany w następujący sposób:

int mul( int x, int y ) {
    return x * y;       // this stores the result of MULtiplying the two variables 
                        // from the stack into the return value address previously 
                        // allocated, then issues a RET, which resets the stack frame
                        // based on the arg list, and returns to the address set by
                        // the CALLer.
}

int main() {
    int x = 2, y = 3;   // these variables are stored on the stack
    mul( x, y );        // this pushes y onto the stack, then x, then a return address,
                        // allocates space on the stack for a return value, 
                        // then issues an assembly CALL instruction.
}

Napisz prosty program taki jak ten, a następnie skompiluj go do asemblera ( gcc -S foo.cjeśli masz dostęp do GCC) i spójrz. Montaż jest dość łatwy do naśladowania. Widać, że stos służy do zmiennych lokalnych funkcji oraz do wywoływania funkcji, przechowywania ich argumentów i zwracanych wartości. Dlatego też, gdy robisz coś takiego:

f( g( h( i ) ) ); 

Wszystkie te są kolejno wywoływane. To dosłownie budowanie stosu wywołań funkcji i ich argumentów, wykonywanie ich, a następnie wyskakiwanie w miarę, jak zawija (lub podnosi;). Jednak, jak wspomniano powyżej, stos (na x86) faktycznie znajduje się w przestrzeni pamięci procesu (w pamięci wirtualnej), więc można nią manipulować bezpośrednio; nie jest to osobny krok podczas wykonywania (lub przynajmniej jest prostopadły do ​​procesu).

Do Twojej wiadomości, powyższa jest konwencją wywoływania C , również używaną przez C ++. Inne języki / systemy mogą wypychać argumenty na stos w innej kolejności, a niektóre języki / platformy nawet nie używają stosów i działają na różne sposoby.

Zauważ też, że nie są to rzeczywiste wiersze wykonywanego kodu C. Kompilator przekonwertował je na instrukcje języka maszynowego w pliku wykonywalnym. Są one następnie (ogólnie) kopiowane z obszaru TEXT do potoku CPU, a następnie do rejestrów CPU i stamtąd wykonywane. [To było niepoprawne. Zobacz poprawkę Bena Voigta poniżej.]


4
przepraszam, ale lepsza odpowiedź na książkę byłaby lepszą odpowiedzią, IMO
Andrey

13
Tak, „RTFM” jest zawsze lepszy.
Sdaz MacSkibbons

56
@Andrey: może powinieneś zmienić ten komentarz na „także, możesz przeczytać zalecenie dotyczące dobrej książki ”. Rozumiem, że tego rodzaju pytanie wymaga dalszych badań, ale za każdym razem, gdy musisz rozpocząć komentarz od „przepraszam, ale. .. ”być może powinieneś rozważyć zgłoszenie postu do moderatora lub przynajmniej wyjaśnienie, dlaczego Twoja opinia i tak powinna mieć znaczenie dla kogokolwiek.
mkelley33

2
Doskonała odpowiedź. Z pewnością wyjaśniło mi to pewne rzeczy!
Maks.

2
@Mikael: W zależności od implementacji może być obowiązkowe buforowanie, w którym to przypadku za każdym razem, gdy dane są odczytywane z pamięci, odczytywana jest cała linia pamięci podręcznej i pamięć podręczna jest zapełniana. Lub może być możliwe wskazanie menedżerowi pamięci podręcznej, że dane będą potrzebne tylko raz, więc skopiowanie ich do pamięci podręcznej nie jest pomocne. To do przeczytania. Do zapisu dostępne są pamięci podręczne z zapisem i z zapisem, które wpływają na to, kiedy kontrolery DMA mogą odczytać dane, a następnie cały szereg protokołów koherencji pamięci podręcznej do obsługi wielu procesorów, z których każdy ma własną pamięć podręczną. To naprawdę zasługuje na swoje własne pytanie.
Ben Voigt

61

Sdaz uzyskał niesamowitą liczbę pozytywnych opinii w bardzo krótkim czasie, ale niestety utrwala błędne przekonanie o tym, jak instrukcje poruszają się przez procesor.

Pytanie zadane:

Instrukcje idą z pamięci RAM, na stos, do rejestrów?

Sdaz powiedział:

Zauważ też, że nie są to rzeczywiste wiersze wykonywanego kodu C. Kompilator przekonwertował je na instrukcje języka maszynowego w pliku wykonywalnym. Są one następnie (ogólnie) kopiowane z obszaru TEXT do potoku CPU, a następnie do rejestrów CPU i stamtąd wykonywane.

Ale to źle. Z wyjątkiem szczególnego przypadku kodu samodmodyfikującego instrukcje nigdy nie wprowadzają ścieżki danych. I nie są, nie mogą być wykonywane z ścieżki danych.

Te rejestry CPU x86 są:

  • Rejestry ogólne EAX EBX ECX EDX

  • Segment rejestruje CS DS ES FS GS SS

  • Indeks i wskaźniki ESI EDI EBP EIP ESP

  • Wskaźnik EFLAGS

Istnieją również rejestry zmiennoprzecinkowe i SIMD, ale do celów niniejszej dyskusji sklasyfikujemy je jako część koprocesora, a nie procesora. Jednostka zarządzania pamięcią wewnątrz procesora ma również własne rejestry, ponownie potraktujemy to jako oddzielną jednostkę przetwarzającą.

Żaden z tych rejestrów nie jest używany dla kodu wykonywalnego. EIPzawiera adres instrukcji wykonawczej, a nie samą instrukcję.

Instrukcje przechodzą przez procesor zupełnie inną ścieżkę niż dane (architektura Harvarda). Wszystkie obecne maszyny mają architekturę Harvarda wewnątrz procesora. Większość tych dni to także architektura Harvarda w pamięci podręcznej. x86 (wspólny komputer stacjonarny) to architektura Von Neumanna w pamięci głównej, co oznacza, że ​​dane i kod są przenikane w pamięci RAM. To nie ma znaczenia, ponieważ mówimy o tym, co dzieje się w procesorze.

Klasyczna sekwencja nauczana w architekturze komputerowej to fetch-decode-execute. Kontroler pamięci wyszukuje instrukcję zapisaną pod adresem EIP. Bity instrukcji przechodzą przez pewną kombinacyjną logikę, aby utworzyć wszystkie sygnały sterujące dla różnych multiplekserów w procesorze. Po kilku cyklach arytmetyczna jednostka logiczna osiąga wynik, który jest taktowany do miejsca docelowego. Następnie pobierana jest kolejna instrukcja.

Na nowoczesnym procesorze wszystko działa trochę inaczej. Każda przychodząca instrukcja jest tłumaczona na całą serię instrukcji mikrokodu. Umożliwia to potokowanie, ponieważ zasoby używane przez pierwszą mikroinstrukcję nie są później potrzebne, więc mogą rozpocząć pracę nad pierwszą mikroinstrukcją od następnej instrukcji.

Podsumowując, terminologia jest nieco mylona, ​​ponieważ rejestr jest terminem inżynierii elektrycznej dla kolekcji D-flipflops. A instrukcje (a zwłaszcza mikroinstrukcje) mogą bardzo dobrze być tymczasowo przechowywane w takiej kolekcji flipflopów. Ale nie to ma na myśli, gdy informatyk, inżynier oprogramowania lub programiści używają terminu rejestr . Oznaczają one rejestry ścieżki danych, jak wymienione powyżej, i nie są one używane do transportu kodu.

Nazwy i liczba rejestrów ścieżek danych są różne dla innych architektur CPU, takich jak ARM, MIPS, Alpha, PowerPC, ale wszystkie wykonują instrukcje bez przekazywania ich przez ALU.


Dziękuję za wyjaśnienie. Z wahaniem dodałem to, ponieważ nie jestem do końca zaznajomiony z tym, ale zrobiłem to na prośbę innej osoby.
Sdaz MacSkibbons

s / ARM / RAM / in ”oznacza, że ​​dane i kod są ze sobą powiązane w ARM”. Dobrze?
Bjarke Freund-Hansen

@bjarkef: Za pierwszym razem tak, ale nie drugi raz. Naprawię to.
Ben Voigt

17

Dokładny układ pamięci podczas wykonywania procesu jest całkowicie zależny od używanej platformy. Rozważ następujący program testowy:

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int stackValue = 0;
    int *addressOnStack = &stackValue;
    int *addressOnHeap = malloc(sizeof(int));
    if (addressOnStack > addressOnHeap)
    {
        puts("The stack is above the heap.");
    }
    else
    {
        puts("The heap is above the stack.");
    }
}

W systemie Windows NT (i jego dzieciach) ten program generalnie generuje:

Kupa znajduje się powyżej stosu

Na pudełkach POSIX będzie napisane:

Stos znajduje się nad stertą

Model pamięci UNIX jest dość dobrze wyjaśniony tutaj przez @Sdaz MacSkibbons, więc nie powtórzę tego tutaj. Ale to nie jedyny model pamięci. Powodem, dla którego POSIX wymaga tego modelu, jest wywołanie systemowe sbrk . Zasadniczo, w pudełku POSIX, aby uzyskać więcej pamięci, proces mówi Kernelowi, aby przesunął dzielnik między „dziurą” a „stertą” dalej do regionu „dziury”. Nie ma sposobu na zwrócenie pamięci do systemu operacyjnego, a sam system operacyjny nie zarządza stertą. Biblioteka środowiska wykonawczego C musi to zapewnić (przez malloc).

Ma to również wpływ na rodzaj kodu faktycznie używanego w plikach binarnych POSIX. Pudełka POSIX (prawie powszechnie) używają formatu pliku ELF. W tym formacie system operacyjny jest odpowiedzialny za komunikację między bibliotekami w różnych plikach ELF. Dlatego wszystkie biblioteki używają kodu niezależnego od pozycji (to znaczy, że sam kod można załadować do różnych adresów pamięci i nadal działać), a wszystkie połączenia między bibliotekami są przekazywane przez tabelę przeglądową, aby dowiedzieć się, gdzie kontrola musi przejść do przejścia wywołania funkcji biblioteki. To powoduje dodatkowe obciążenie i może zostać wykorzystane, jeśli jedna z bibliotek zmieni tabelę wyszukiwania.

Model pamięci systemu Windows jest inny, ponieważ używany przez niego kod jest inny. Windows używa formatu pliku PE, który pozostawia kod w formacie zależnym od pozycji. Oznacza to, że kod zależy od tego, gdzie dokładnie w pamięci wirtualnej jest ładowany kod. W specyfikacji PE znajduje się flaga, która informuje system operacyjny, gdzie dokładnie w pamięci biblioteka lub plik wykonywalny chciałby zostać zmapowany podczas działania programu. Jeśli programu lub biblioteki nie można załadować pod preferowanym adresem, moduł ładujący systemu Windows musi zmienićbiblioteka / plik wykonywalny - w zasadzie przesuwa kod zależny od pozycji, aby wskazywał nowe pozycje - co nie wymaga tabel odnośników i nie może być wykorzystane, ponieważ nie ma tablic odnośników do zastąpienia. Niestety wymaga to bardzo skomplikowanej implementacji w module ładującym systemu Windows i wymaga znacznego czasu uruchamiania, jeśli obraz wymaga zmiany bazy. Duże komercyjne pakiety oprogramowania często modyfikują swoje biblioteki, aby celowo uruchamiać pod różnymi adresami, aby uniknąć zmiany bazy; sam system Windows robi to z własnymi bibliotekami (np. ntdll.dll, kernel32.dll, psapi.dll itp. - wszystkie mają domyślnie różne adresy początkowe)

W systemie Windows pamięć wirtualna jest uzyskiwana z systemu przez wywołanie VirtualAlloc i jest zwracana do systemu przez VirtualFree (OK, technicznie VirtualAlloc farmuje się do NtAllocateVirtualMemory, ale to szczegół implementacji) (Porównaj to z POSIX, gdzie pamięć nie może odzyskać). Proces ten jest powolny (a IIRC wymaga alokacji na porcje wielkości strony fizycznej; zwykle 4 KB lub więcej). System Windows udostępnia również własne funkcje sterty (HeapAlloc, HeapFree itp.) Jako część biblioteki znanej jako RtlHeap, która jest zawarta jako część samego systemu Windows, na której środowisko wykonawcze C (to znaczymalloc zazwyczaj jest wdrażane i przyjaciele).

System Windows ma również kilka starszych interfejsów API alokacji pamięci z czasów, gdy miał do czynienia ze starymi modelami 80386, a funkcje te są teraz wbudowane w interfejs RtlHeap. Więcej informacji o różnych interfejsach API sterujących zarządzaniem pamięcią w systemie Windows znajduje się w tym artykule MSDN: http://msdn.microsoft.com/en-us/library/ms810627 .

Należy również zauważyć, że oznacza to, że w systemie Windows pojedynczy proces (i zwykle ma) ma więcej niż jedną stertę. (Zazwyczaj każda biblioteka współdzielona tworzy własną stertę).

(Większość tych informacji pochodzi z „Bezpiecznego kodowania w C i C ++” Roberta Seacorda)


Świetna informacja, dzięki! Mam nadzieję, że „user487117” w końcu rzeczywiście powróci. :-)
Sdaz MacSkibbons

5

Stos

W architekturze X86 CPU wykonuje operacje na rejestrach. Stos jest używany tylko ze względów wygody. Możesz zapisać zawartość swoich rejestrów w stosie przed wywołaniem podprogramu lub funkcji systemowej, a następnie załadować je z powrotem, aby kontynuować operację tam, gdzie opuściłeś. (Możesz to zrobić ręcznie bez stosu, ale jest to często używana funkcja, więc ma wsparcie procesora). Ale możesz zrobić prawie wszystko bez stosu w komputerze.

Na przykład mnożenie liczb całkowitych:

MUL BX

Mnoży rejestr AX z rejestrem BX. (Wynik będzie w DX i AX, DX zawierający wyższe bity).

Maszyny oparte na stosie (takie jak JAVA VM) używają stosu do swoich podstawowych operacji. Powyższe mnożenie:

DMUL

Spowoduje to wyświetlenie dwóch wartości z góry stosu i pomnożenie tem, a następnie wypchnięcie wyniku z powrotem do stosu. Stos jest niezbędny dla tego rodzaju maszyn.

Niektóre języki programowania wyższego poziomu (takie jak C i Pascal) używają tej późniejszej metody do przekazywania parametrów do funkcji: parametry są wypychane na stos w kolejności od lewej do prawej, a następnie wstawiane przez treść funkcji, a zwracane wartości są wypychane z powrotem. (Jest to wybór dokonywany przez producentów kompilatorów i rodzaj nadużywania sposobu, w jaki X86 używa stosu).

Kupa

Sterta to kolejna koncepcja, która istnieje tylko w dziedzinie kompilatorów. To zabiera ból związany z obsługą pamięci za zmiennymi, ale nie jest to funkcja procesora ani systemu operacyjnego, to tylko wybór sposobu utrzymania bloku pamięci, który jest wydawany przez system operacyjny. Możesz to zrobić na wiele sposobów, jeśli chcesz.

Dostęp do zasobów systemowych

System operacyjny ma publiczny interfejs umożliwiający dostęp do jego funkcji. W DOS parametry są przekazywane do rejestrów CPU. System Windows używa stosu do przekazywania parametrów funkcji systemu operacyjnego (Windows API).

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.