Realmente depende del sistema, pero los sistemas operativos modernos con memoria virtual tienden a cargar sus imágenes de proceso y asignar memoria a algo como esto:
+---------+
| 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.
+---------+
Este es el espacio de direcciones de proceso general en muchos sistemas comunes de memoria virtual. El "agujero" es el tamaño de su memoria total, menos el espacio ocupado por todas las otras áreas; Esto proporciona una gran cantidad de espacio para que el montón crezca. Esto también es "virtual", lo que significa que se asigna a su memoria real a través de una tabla de traducción, y puede almacenarse en cualquier lugar en la memoria real. Se hace de esta manera para proteger un proceso de acceder a la memoria de otro proceso y hacer que cada proceso piense que se está ejecutando en un sistema completo.
Tenga en cuenta que las posiciones de, por ejemplo, la pila y el montón pueden estar en un orden diferente en algunos sistemas (consulte la respuesta de Billy O'Neal a continuación para obtener más detalles sobre Win32).
Otros sistemas pueden ser muy diferentes. DOS, por ejemplo, se ejecutó en modo real , y su asignación de memoria al ejecutar programas se veía de manera muy diferente:
+-----------+ 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
Puede ver que DOS permitió el acceso directo a la memoria del sistema operativo, sin protección, lo que significaba que los programas de espacio de usuario generalmente podían acceder o sobrescribir directamente lo que quisieran.
Sin embargo, en el espacio de direcciones del proceso, los programas tendieron a parecerse, solo se describieron como segmento de código, segmento de datos, montón, segmento de pila, etc., y se asignó de manera un poco diferente. Pero la mayoría de las áreas generales todavía estaban allí.
Al cargar el programa y las bibliotecas compartidas necesarias en la memoria, y distribuir las partes del programa en las áreas correctas, el sistema operativo comienza a ejecutar su proceso donde sea que esté su método principal, y su programa toma el control desde allí, haciendo llamadas al sistema según sea necesario cuando los necesita
Los diferentes sistemas (incrustados, lo que sea) pueden tener arquitecturas muy diferentes, como los sistemas sin pila, los sistemas de arquitectura de Harvard (con código y datos que se mantienen en una memoria física separada), sistemas que realmente mantienen el BSS en la memoria de solo lectura (inicialmente establecida por el programador), etc. Pero esta es la esencia general.
Tu dijiste:
También sé que un programa de computadora usa dos tipos de memoria: pila y montón, que también son parte de la memoria primaria de la computadora.
"Pila" y "montón" son solo conceptos abstractos, en lugar de (necesariamente) "tipos" de memoria físicamente distintos.
Una pila es simplemente una estructura de datos de último en entrar, primero en salir. En la arquitectura x86, en realidad se puede abordar aleatoriamente utilizando un desplazamiento desde el final, pero las funciones más comunes son PUSH y POP para agregar y eliminar elementos, respectivamente. Se usa comúnmente para variables locales de función (denominado "almacenamiento automático"), argumentos de función, direcciones de retorno, etc. (más abajo)
Un "montón" es solo un apodo para un trozo de memoria que se puede asignar a pedido y se trata de forma aleatoria (es decir, puede acceder a cualquier ubicación directamente). Se usa comúnmente para estructuras de datos que asigna en tiempo de ejecución (en C ++, usando new
y delete
, malloc
y amigos en C, etc.).
La pila y el montón, en la arquitectura x86, residen físicamente en la memoria del sistema (RAM) y se asignan a través de la asignación de memoria virtual en el espacio de direcciones del proceso como se describió anteriormente.
Los registros (aún en x86), residen físicamente dentro del procesador (a diferencia de la RAM), y son cargados por el procesador, desde el área de TEXTO (y también pueden cargarse desde otro lugar en la memoria u otros lugares dependiendo de las instrucciones de la CPU que son realmente ejecutados). Básicamente son ubicaciones de memoria en chip muy pequeñas y muy rápidas que se utilizan para diferentes propósitos.
El diseño del registro depende en gran medida de la arquitectura (de hecho, los registros, el conjunto de instrucciones y el diseño / diseño de la memoria son exactamente lo que se entiende por "arquitectura"), por lo que no lo ampliaré, pero le recomiendo que tome un curso de lenguaje ensamblador para entenderlos mejor.
Tu pregunta:
¿En qué punto se utiliza la pila para la ejecución de las instrucciones? ¿Las instrucciones van desde la RAM, a la pila, a los registros?
La pila (en sistemas / idiomas que los tienen y los usan) se usa con mayor frecuencia de esta manera:
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.
}
Escriba un programa simple como este, y luego compílelo en ensamblador ( gcc -S foo.c
si tiene acceso a GCC), y eche un vistazo. El montaje es bastante fácil de seguir. Puede ver que la pila se usa para variables locales de función y para llamar a funciones, almacenar sus argumentos y valores de retorno. Esta es también la razón por la que haces algo como:
f( g( h( i ) ) );
Todos estos se llaman a su vez. Literalmente, está acumulando una pila de llamadas a funciones y sus argumentos, ejecutándolas y luego haciéndolas explotar a medida que retrocede (o sube). Sin embargo, como se mencionó anteriormente, la pila (en x86) en realidad reside en el espacio de memoria de su proceso (en la memoria virtual), por lo que puede manipularse directamente; no es un paso separado durante la ejecución (o al menos es ortogonal al proceso).
Para su información, lo anterior es la convención de llamada C , también utilizada por C ++. Otros lenguajes / sistemas pueden insertar argumentos en la pila en un orden diferente, y algunos lenguajes / plataformas ni siquiera usan pilas, y lo hacen de diferentes maneras.
También tenga en cuenta que estas no son líneas reales de ejecución de código C. El compilador los ha convertido en instrucciones de lenguaje de máquina en su ejecutable. Luego (generalmente) se copian del área de TEXTO a la tubería de la CPU, luego a los registros de la CPU y se ejecutan desde allí. [Esto fue incorrecto. Ver la corrección de Ben Voigt a continuación.]