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 new
e delete
, e malloc
e 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.c
se 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.]