Cosa succede quando viene eseguito un programma per computer?


180

Conosco la teoria generale ma non riesco a inserirmi nei dettagli.

So che un programma risiede nella memoria secondaria di un computer. Una volta avviata l'esecuzione del programma, questo viene interamente copiato nella RAM. Quindi il processore recupera alcune istruzioni (dipende dalla dimensione del bus) alla volta, le inserisce nei registri e le esegue.

So anche che un programma per computer utilizza due tipi di memoria: stack e heap, che fanno anche parte della memoria principale del computer. Lo stack viene utilizzato per la memoria non dinamica e l'heap per la memoria dinamica (ad esempio, tutto ciò che riguarda l' newoperatore in C ++)

Quello che non riesco a capire è come si collegano queste due cose. A che punto viene utilizzato lo stack per l'esecuzione delle istruzioni? Le istruzioni vanno dalla RAM, allo stack, ai registri?


43
+1 per aver posto una domanda fondamentale!
mkelley33

21
hmm ... sai, scrivono libri su questo. Vuoi davvero studiare questa parte dell'architettura del sistema operativo con l'aiuto di SO?
Andrey,

1
Ho aggiunto un paio di tag in base alla natura della memoria relativa alla domanda e al riferimento a C ++, anche se penso che una buona risposta potrebbe anche provenire da qualcuno che conosce Java e C #!)
mkelley33

14
Votato e favorito. Ho sempre avuto troppa paura di chiedere ...
Maxpm

2
Il termine "li inserisce nei registri" non è del tutto corretto. Sulla maggior parte dei processori, i registri vengono utilizzati per contenere valori intermedi, non codice eseguibile.

Risposte:


161

Dipende molto dal sistema, ma i moderni sistemi operativi con memoria virtuale tendono a caricare le loro immagini di processo e allocare memoria in questo modo:

+---------+
|  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.
+---------+

Questo è lo spazio degli indirizzi di processo generale su molti comuni sistemi di memoria virtuale. Il "buco" è la dimensione della tua memoria totale, meno lo spazio occupato da tutte le altre aree; questo dà una grande quantità di spazio per far crescere l'heap. Anche questo è "virtuale", il che significa che è mappato alla tua memoria effettiva attraverso una tabella di traduzione e può essere effettivamente archiviato in qualsiasi posizione nella memoria effettiva. Viene fatto in questo modo per proteggere un processo dall'accesso alla memoria di un altro processo e per far sì che ciascun processo pensi che sia in esecuzione su un sistema completo.

Si noti che le posizioni, ad esempio, dello stack e dell'heap potrebbero essere in un ordine diverso su alcuni sistemi (vedere la risposta di Billy O'Neal di seguito per maggiori dettagli su Win32).

Altri sistemi possono essere molto diversi. DOS, ad esempio, veniva eseguito in modalità reale e la sua allocazione di memoria durante l'esecuzione dei programmi sembrava molto diversa:

+-----------+ 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

Puoi vedere che DOS ha consentito l'accesso diretto alla memoria del sistema operativo, senza protezione, il che significa che i programmi di spazio utente potevano generalmente accedere o sovrascrivere direttamente qualsiasi cosa gli piacesse.

Nello spazio degli indirizzi di processo, tuttavia, i programmi tendevano ad apparire simili, solo che venivano descritti come segmento di codice, segmento di dati, heap, segmento di stack, ecc. Ed era mappato in modo leggermente diverso. Ma la maggior parte delle aree generali erano ancora lì.

Dopo aver caricato il programma e le necessarie librerie condivise in memoria e distribuito le parti del programma nelle aree giuste, il sistema operativo inizia a eseguire il processo ovunque si trovi il suo metodo principale e il programma prende il posto da lì, effettuando le chiamate di sistema quando necessario ne ha bisogno.

Sistemi diversi (incorporati, qualunque cosa) possono avere architetture molto diverse, come i sistemi stackless, i sistemi di architettura Harvard (con codice e dati mantenuti in memoria fisica separata), sistemi che mantengono effettivamente il BSS nella memoria di sola lettura (inizialmente impostato dal programmatore), ecc. Ma questa è l'essenza generale.


Tu hai detto:

So anche che un programma per computer utilizza due tipi di memoria: stack e heap, che fanno anche parte della memoria principale del computer.

"Stack" e "heap" sono solo concetti astratti, piuttosto che "tipi" di memoria (necessariamente) fisicamente distinti.

Uno stack è semplicemente una struttura dati last-in, first-out. Nell'architettura x86, può effettivamente essere indirizzato in modo casuale utilizzando un offset dalla fine, ma le funzioni più comuni sono PUSH e POP per aggiungere e rimuovere elementi da esso, rispettivamente. È comunemente usato per variabili locali di funzione (il cosiddetto "archivio automatico"), argomenti di funzioni, indirizzi di ritorno, ecc. (Più sotto)

Un "heap" è solo un soprannome per un pezzo di memoria che può essere allocato su richiesta e viene indirizzato in modo casuale (il che significa che è possibile accedere direttamente a qualsiasi posizione in esso). È comunemente usato per le strutture di dati che si allocano in fase di esecuzione (in C ++, usando newe delete, e malloce amici in C, ecc.).

Lo stack e l'heap, sull'architettura x86, risiedono entrambi fisicamente nella memoria del sistema (RAM) e sono mappati attraverso l'allocazione della memoria virtuale nello spazio degli indirizzi di processo come descritto sopra.

I registri (ancora su x86), risiedono fisicamente all'interno del processore (al contrario della RAM) e sono caricati dal processore, dall'area TEXT (e possono anche essere caricati altrove in memoria o in altri luoghi a seconda delle istruzioni della CPU che sono effettivamente eseguiti). Sono essenzialmente posizioni di memoria su chip molto piccole e molto veloci che vengono utilizzate per diversi scopi.

Il layout dei registri dipende fortemente dall'architettura (in effetti registri, set di istruzioni e layout / progettazione della memoria, sono esattamente ciò che si intende per "architettura"), quindi non mi espanderò su di esso, ma consiglio di prendere un corso di assemblaggio per capirli meglio.


La tua domanda:

A che punto viene utilizzato lo stack per l'esecuzione delle istruzioni? Le istruzioni vanno dalla RAM, allo stack, ai registri?

Lo stack (in sistemi / lingue che li hanno e li usano) viene spesso usato in questo modo:

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.
}

Scrivi un programma semplice come questo, quindi compilarlo in assembly ( gcc -S foo.cse hai accesso a GCC) e dai un'occhiata. Il montaggio è piuttosto semplice da seguire. Si può vedere che lo stack viene utilizzato per le variabili locali delle funzioni e per chiamare le funzioni, archiviando i loro argomenti e restituendo i valori. Questo è anche il motivo per cui quando fai qualcosa del genere:

f( g( h( i ) ) ); 

Tutti questi vengono chiamati a turno. Sta letteralmente costruendo una pila di chiamate di funzioni e i loro argomenti, eseguendole e poi saltandole via mentre si riavvolge (o su;). Tuttavia, come menzionato sopra, lo stack (su x86) risiede effettivamente nello spazio di memoria del processo (nella memoria virtuale) e quindi può essere manipolato direttamente; non è un passaggio separato durante l'esecuzione (o almeno è ortogonale al processo).

Cordiali saluti, quanto sopra è la convenzione di chiamata C , utilizzata anche da C ++. Altre lingue / sistemi possono spingere gli argomenti nello stack in un ordine diverso, e alcune lingue / piattaforme non usano nemmeno stack, e lo fanno in modi diversi.

Inoltre, queste non sono righe effettive dell'esecuzione del codice C. Il compilatore li ha convertiti in istruzioni in linguaggio macchina nel tuo eseguibile. Vengono quindi (generalmente) copiati dall'area TEXT nella pipeline della CPU, quindi nei registri della CPU ed eseguiti da lì. [Questo non era corretto. Vedi la correzione di Ben Voigt di seguito.]


4
scusate, ma una buona raccomandazione per un libro sarebbe una risposta migliore, IMO
Andrey

13
Sì, "RTFM" è sempre meglio.
Sdaz MacSkibbons

56
@Andrey: forse dovresti cambiare quel commento in "anche, potresti voler leggere il tuo-buon-libro-raccomandazione " Capisco che questo tipo di domanda merita ulteriori indagini, ma ogni volta che devi iniziare un commento con "scusa ma. .. "forse dovresti davvero considerare di contrassegnare il post per l'attenzione del moderatore o almeno di offrire una spiegazione sul perché la tua opinione dovrebbe comunque interessare a chiunque.
mkelley33

2
Risposta eccellente. Certamente ha chiarito alcune cose per me!
Max

2
@Mikael: a seconda dell'implementazione, potresti avere una cache obbligatoria, nel qual caso ogni volta che i dati vengono letti dalla memoria, viene letta un'intera riga della cache e la cache viene popolata. Oppure potrebbe essere possibile dare al gestore della cache un suggerimento che i dati saranno necessari solo una volta, quindi non è utile copiarli nella cache. Questo è per leggere. Per la scrittura ci sono cache write-back e write-through, che influiscono quando i controller DMA sono in grado di leggere i dati, e poi c'è un intero host di protocolli di coerenza della cache per gestire più processori ciascuno con la propria cache. Questo merita davvero il suo Q.
Ben Voigt

61

Sdaz ha ottenuto un numero notevole di voti in pochissimo tempo, ma purtroppo sta perpetuando un malinteso su come le istruzioni si muovono attraverso la CPU.

La domanda è stata posta:

Le istruzioni vanno dalla RAM, allo stack, ai registri?

Sdaz ha detto:

Inoltre, queste non sono righe effettive dell'esecuzione del codice C. Il compilatore li ha convertiti in istruzioni in linguaggio macchina nel tuo eseguibile. Vengono quindi (generalmente) copiati dall'area TEXT nella pipeline della CPU, quindi nei registri della CPU ed eseguiti da lì.

Ma questo è sbagliato. Ad eccezione del caso speciale di codice auto-modificante, le istruzioni non entrano mai nel datapath. E non sono, non possono essere, eseguiti dal datapath.

I registri della CPU x86 sono:

  • Registri generali EAX EBX ECX EDX

  • Registri di segmento CS DS ES FS GS SS

  • Indice e puntatori ESI EDI EBP EIP ESP

  • Indicatore EFLAGS

Esistono anche alcuni registri a virgola mobile e SIMD, ma ai fini di questa discussione classificheremo quelli come parte del coprocessore e non della CPU. L'unità di gestione della memoria all'interno della CPU ha anche alcuni registri propri, lo tratteremo nuovamente come un'unità di elaborazione separata.

Nessuno di questi registri viene utilizzato per il codice eseguibile. EIPcontiene l'indirizzo dell'istruzione di esecuzione, non l'istruzione stessa.

Le istruzioni percorrono un percorso completamente diverso nella CPU dai dati (architettura di Harvard). Tutte le macchine attuali sono architettura Harvard all'interno della CPU. Molti di questi giorni sono anche l'architettura di Harvard nella cache. x86 (la tua macchina desktop comune) sono l'architettura Von Neumann nella memoria principale, il che significa che dati e codice sono mescolati nella RAM. Questo è il punto, dal momento che stiamo parlando di ciò che accade all'interno della CPU.

La sequenza classica insegnata nell'architettura del computer è fetch-decode-execute. Il controller di memoria cerca le istruzioni memorizzate all'indirizzo EIP. I bit dell'istruzione passano attraverso una logica combinatoria per creare tutti i segnali di controllo per i diversi multiplexer nel processore. E dopo alcuni cicli, l'unità logica aritmetica arriva a un risultato, che è sincronizzato nella destinazione. Quindi viene recuperata l'istruzione successiva.

Su un processore moderno, le cose funzionano in modo leggermente diverso. Ogni istruzione in arrivo viene tradotta in un'intera serie di istruzioni per microcodici. Questo abilita il pipelining, perché le risorse utilizzate dalla prima microistruzione non sono necessarie in seguito, quindi possono iniziare a lavorare sulla prima microistruzione dall'istruzione successiva.

Per finire, la terminologia è leggermente confusa perché registro è un termine di ingegneria elettrica per una raccolta di infradito. E le istruzioni (o in particolare le microistruzioni) possono essere memorizzate temporaneamente in una tale raccolta di infradito. Ma questo non è ciò che si intende quando un informatico, un ingegnere informatico o uno sviluppatore abituale usa il termine registro . Significano i registri datapath come elencato sopra e questi non vengono utilizzati per il trasporto del codice.

I nomi e il numero dei registri del datapath variano per altre architetture di CPU, come ARM, MIPS, Alpha, PowerPC, ma tutte eseguono le istruzioni senza passarle attraverso l'ALU.


Grazie per il chiarimento. Ero titubante nell'aggiungerlo perché non ne ho familiarità, ma lo ho fatto su richiesta di qualcun altro.
Sdaz MacSkibbons

s / ARM / RAM / in "significato dati e codice sono mescolati in ARM". Destra?
Bjarke Freund-Hansen

@bjarkef: la prima volta sì, ma non la seconda. Lo aggiusterò.
Ben Voigt

17

Il layout esatto della memoria durante l'esecuzione di un processo dipende completamente dalla piattaforma che si sta utilizzando. Considera il seguente programma di test:

#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.");
    }
}

Su Windows NT (ed è figlio), questo programma generalmente produrrà:

L'heap è sopra la pila

Sulle caselle POSIX, dirà:

La pila è sopra l'heap

Il modello di memoria UNIX è abbastanza ben spiegato qui da @Sdaz MacSkibbons, quindi non lo ripeterò qui. Ma questo non è l'unico modello di memoria. Il motivo per cui POSIX richiede questo modello è la chiamata di sistema sbrk . Fondamentalmente, su una casella POSIX, per ottenere più memoria, un processo dice semplicemente al kernel di spostare il divisore tra il "buco" e il "mucchio" più lontano nella regione del "buco". Non è possibile restituire memoria al sistema operativo e il sistema operativo stesso non gestisce l'heap. La tua libreria C runtime deve fornire ciò (tramite malloc).

Ciò ha anche implicazioni per il tipo di codice effettivamente utilizzato nei binari POSIX. Le caselle POSIX (quasi universalmente) usano il formato di file ELF. In questo formato, il sistema operativo è responsabile delle comunicazioni tra le librerie in diversi file ELF. Pertanto, tutte le librerie utilizzano un codice indipendente dalla posizione (ovvero, il codice stesso può essere caricato in diversi indirizzi di memoria e continuare a funzionare) e tutte le chiamate tra le librerie vengono passate attraverso una tabella di ricerca per scoprire dove il controllo deve saltare per attraversare chiamate alla funzione di libreria. Ciò aggiunge un certo sovraccarico e può essere sfruttato se una delle librerie modifica la tabella di ricerca.

Il modello di memoria di Windows è diverso perché il tipo di codice che utilizza è diverso. Windows utilizza il formato di file PE, che lascia il codice in formato dipendente dalla posizione. Cioè, il codice dipende da dove viene caricato esattamente nella memoria virtuale. C'è un flag nelle specifiche PE che dice al sistema operativo dove esattamente in memoria la libreria o l'eseguibile vorrebbero essere mappati quando il programma viene eseguito. Se un programma o una libreria non possono essere caricati al suo indirizzo preferito, il caricatore di Windows deve rifarsila libreria / eseguibile - sostanzialmente, sposta il codice dipendente dalla posizione in modo che punti alle nuove posizioni - che non richiede tabelle di ricerca e non può essere sfruttato perché non esiste alcuna tabella di ricerca da sovrascrivere. Sfortunatamente, questo richiede un'implementazione molto complicata nel caricatore di Windows e ha un notevole sovraccarico di avvio se un'immagine deve essere rivista. Grandi pacchetti di software commerciali spesso modificano le loro librerie per iniziare intenzionalmente a indirizzi diversi per evitare la riformulazione; Windows stesso fa questo con le sue librerie (ad esempio ntdll.dll, kernel32.dll, psapi.dll, ecc. - tutti hanno indirizzi iniziali diversi per impostazione predefinita)

Su Windows, la memoria virtuale viene ottenuta dal sistema tramite una chiamata a VirtualAlloc e viene restituita al sistema tramite VirtualFree (Ok, tecnicamente VirtualAlloc esegue il farm su NtAllocateVirtualMemory, ma questo è un dettaglio di implementazione) (Contrast this to POSIX, dove memory non può essere recuperato). Questo processo è lento (e IIRC, richiede l'allocazione in blocchi di dimensioni fisiche di pagina; in genere 4kb o più). Windows fornisce anche le proprie funzioni heap (HeapAlloc, HeapFree, ecc.) Come parte di una libreria nota come RtlHeap, che è inclusa come parte dello stesso Windows, su cui mallocviene generalmente implementato il runtime C (cioè e amici).

Windows ha anche alcune API di allocazione della memoria legacy dai tempi in cui doveva occuparsi dei vecchi 80386 e queste funzioni sono ora costruite su RtlHeap. Per ulteriori informazioni sulle varie API che controllano la gestione della memoria in Windows, consultare questo articolo MSDN: http://msdn.microsoft.com/en-us/library/ms810627 .

Si noti inoltre che ciò significa che su Windows un singolo processo (e in genere lo fa) ha più di un heap. (In genere, ogni libreria condivisa crea il proprio heap.)

(La maggior parte di queste informazioni proviene da "Codifica sicura in C e C ++" di Robert Seacord)


Grandi informazioni, grazie! Spero che "user487117" alla fine ritorni effettivamente. :-)
Sdaz MacSkibbons

5

Lo stack

In X86 architercture la CPU esegue le operazioni con i registri. Lo stack viene utilizzato solo per motivi di praticità. È possibile salvare il contenuto dei registri da impilare prima di chiamare una subroutine o una funzione di sistema, quindi ricaricarli per continuare l'operazione da dove si è lasciato. (Potresti farlo manualmente senza lo stack, ma è una funzione usata frequentemente quindi ha il supporto CPU). Ma puoi fare praticamente qualsiasi cosa senza lo stack in un PC.

Ad esempio una moltiplicazione intera:

MUL BX

Moltiplica il registro AX con il registro BX. (Il risultato sarà in DX e AX, DX contenente i bit più alti).

Le macchine basate su stack (come JAVA VM) usano lo stack per le loro operazioni di base. La moltiplicazione sopra:

DMUL

Questo fa apparire due valori dalla cima dello stack e moltiplica il tem, quindi riporta il risultato nello stack. Lo stack è essenziale per questo tipo di macchine.

Alcuni linguaggi di programmazione di livello superiore (come C e Pascal) usano questo metodo successivo per passare parametri alle funzioni: i parametri vengono spinti nello stack nell'ordine da sinistra a destra e spuntati dal corpo della funzione e i valori di ritorno vengono respinti. (Questa è una scelta che i produttori di compilatori fanno e che abusano del modo in cui l'X86 usa lo stack).

Il mucchio

L'heap è un altro concetto che esiste solo nel regno dei compilatori. Prende il disturbo di gestire la memoria dietro le variabili, ma non è una funzione della CPU o del sistema operativo, è solo una scelta di pulizia del blocco di memoria che viene dato dal sistema operativo. Puoi farlo molte volte se vuoi.

Accesso alle risorse di sistema

Il sistema operativo ha un'interfaccia pubblica su come accedere alle sue funzioni. In DOS i parametri vengono passati nei registri della CPU. Windows utilizza lo stack per passare parametri per le funzioni del sistema operativo (l'API di Windows).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.