programas programacion programa para lenguaje fundamentos ejecutar compilar c++ memory operating-system x86 computer-architecture

programacion - programa para compilar c++



¿Qué sucede cuando se ejecuta un programa de computadora? (4)

Conozco la teoría general, pero no puedo encajar en los detalles.

Sé que un programa reside en la memoria secundaria de una computadora. Una vez que el programa comienza la ejecución, se copia por completo a la memoria RAM. Luego, el procesador recupera algunas instrucciones (depende del tamaño del bus) a la vez, las coloca en registros y las ejecuta.

También sé que un programa de computadora usa dos tipos de memoria: pila y pila, que también son parte de la memoria primaria de la computadora. La pila se usa para memoria no dinámica y el montón para memoria dinámica (por ejemplo, todo lo relacionado con el new operador en C ++)

Lo que no puedo entender es cómo se conectan esas dos cosas. ¿En qué punto se usa la pila para la ejecución de las instrucciones? Las instrucciones van desde la RAM, a la pila, a los registros?


Depende realmente del sistema, pero los sistemas operativos modernos con memoria virtual tienden a cargar sus imágenes de proceso y a 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 de memoria virtual comunes. El "agujero" es el tamaño de la memoria total, menos el espacio ocupado por todas las otras áreas; esto le da 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 ubicación de la memoria real. Se hace de esta manera para proteger un proceso del acceso a la memoria de otro proceso, y para hacer que cada proceso crea 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 permite el acceso directo a la memoria del sistema operativo, sin protección, lo que significa que los programas de espacio de usuario generalmente pueden acceder directamente o sobrescribir lo que les gusta.

Sin embargo, en el espacio de direcciones de proceso, los programas tendían a parecer similares, solo que se describían como segmento de código, segmento de datos, montón, segmento de pila, etc., y se mapearon de forma un poco diferente. Pero la mayoría de las áreas generales todavía estaban allí.

Al cargar el programa y las librerías compartidas necesarias en la memoria y distribuir las partes del programa en las áreas correctas, el SO comienza a ejecutar el proceso donde sea que esté su método principal, y su programa toma el control desde allí, haciendo las llamadas al sistema necesarias cuando los necesita.

Los diferentes sistemas (incrustados, lo que sea) pueden tener arquitecturas muy diferentes, como sistemas sin apilamiento, sistemas de arquitectura de Harvard (con código y datos guardados en memoria física separada), sistemas que realmente mantienen el BSS en memoria de solo lectura (inicialmente establecido por 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 pila, que también son parte de la memoria primaria de la computadora.

"Pila" y "montón" son solo conceptos abstractos, en lugar de "necesariamente" físicamente distintos "tipos" de memoria.

Una stack es simplemente una estructura de datos de último en entrar, primero en salir. En la arquitectura x86, en realidad se puede abordar de forma aleatoria utilizando un desplazamiento desde el final, pero las funciones más comunes son PUSH y POP para agregar y eliminar elementos de él, respectivamente. Se usa comúnmente para variables locales de funciones (el denominado "almacenamiento automático"), argumentos de función, direcciones de retorno, etc. (más abajo)

Un "heap" es solo un apodo para un trozo de memoria que se puede asignar a pedido y se direcciona de forma aleatoria (es decir, puede acceder a cualquier ubicación en él directamente). Se usa comúnmente para estructuras de datos que usted asigna en tiempo de ejecución (en C ++, usando new y delete , y 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 registers (aún en x86) residen físicamente dentro del procesador (a diferencia de la RAM) y el procesador los carga desde el área de TEXTO (y también se pueden cargar desde otro lugar en la memoria u otros lugares dependiendo de las instrucciones de la CPU que en realidad se ejecutan). En esencia, son ubicaciones de memoria en chip muy pequeñas y muy rápidas que se utilizan para una serie de propósitos diferentes.

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 voy a ampliarlo, pero le recomiendo que tome una curso de lenguaje ensamblado para entenderlos mejor.

Tu pregunta:

¿En qué punto se usa 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 usa) se usa más a menudo 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 para ensamblar ( 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 funciones de llamada, almacenar sus argumentos y devolver valores. Esta también es la razón por la cual cuando haces algo como:

f( g( h( i ) ) );

Todos estos son llamados a su vez. Literalmente está acumulando una pila de llamadas a funciones y sus argumentos, ejecutándolos y luego apagándolos a medida que avanza hacia abajo (o hacia arriba;). Sin embargo, como se mencionó anteriormente, la pila (en x86) en realidad reside en el espacio de la memoria de 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, la anterior es la convención de llamadas de C , también utilizada por C ++. Otros idiomas / sistemas pueden insertar argumentos en la pila en un orden diferente, y algunos lenguajes / plataformas ni siquiera usan stacks, 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 se copian (generalmente) desde el área TEXT 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.]


El diseño exacto de la memoria mientras se ejecuta un proceso depende completamente de la plataforma que esté utilizando. Considere el siguiente programa de prueba:

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

En Windows NT (y es hijos), este programa generalmente producirá:

El montón está por encima de la pila

En los cuadros POSIX, va a decir:

La pila está por encima del montón

El modelo de memoria de UNIX está muy bien explicado aquí por @Sdaz MacSkibbons, así que no voy a reiterarlo aquí. Pero ese no es el único modelo de memoria. La razón por la que POSIX requiere que este modelo sea la sbrk sistema sbrk . Básicamente, en una caja POSIX, para obtener más memoria, un proceso simplemente le dice al Kernel que mueva el divisor entre el "agujero" y el "montón" más adentro de la región del "agujero". No hay forma de devolver la memoria al sistema operativo, y el sistema operativo en sí no administra su montón. Su biblioteca C runtime tiene que proporcionar eso (a través de malloc).

Esto también tiene implicaciones para el tipo de código realmente utilizado en los binarios POSIX. Los cuadros POSIX (casi universalmente) usan el formato de archivo ELF. En este formato, el sistema operativo es responsable de las comunicaciones entre bibliotecas en diferentes archivos ELF. Por lo tanto, todas las bibliotecas usan código independiente de posición (es decir, el código puede cargarse en diferentes direcciones de memoria y seguir funcionando) y todas las llamadas entre bibliotecas se pasan a través de una tabla de búsqueda para averiguar dónde debe saltar el control para cruzar llamadas de función de biblioteca. Esto agrega algunos gastos generales y se puede aprovechar si una de las bibliotecas cambia la tabla de búsqueda.

El modelo de memoria de Windows es diferente porque el tipo de código que utiliza es diferente. Windows usa el formato de archivo PE, que deja el código en formato dependiente de la posición. Es decir, el código depende de dónde exactamente se carga el código en la memoria virtual. Hay un indicador en la especificación PE que le dice al sistema operativo dónde está exactamente en la memoria la biblioteca o el ejecutable que desea asignar cuando se ejecuta su programa. Si un programa o biblioteca no se puede cargar en su dirección preferida, el cargador de Windows debe volver a establecer la base de la biblioteca / ejecutable; básicamente, mueve el código dependiente de la posición para señalar las nuevas posiciones, que no requiere tablas de búsqueda y no puede ser explotado porque no hay una tabla de búsqueda para sobrescribir. Desafortunadamente, esto requiere una implementación muy complicada en el cargador de Windows, y tiene una considerable sobrecarga de tiempo de inicio si una imagen necesita ser actualizada. Los grandes paquetes de software comercial a menudo modifican sus bibliotecas para comenzar deliberadamente en diferentes direcciones para evitar el rebase; Windows mismo hace esto con sus propias librerías (por ejemplo, ntdll.dll, kernel32.dll, psapi.dll, etc.) todas tienen direcciones de inicio diferentes por defecto)

En Windows, la memoria virtual se obtiene del sistema a través de una llamada a VirtualAlloc , y se devuelve al sistema a través de VirtualFree (está bien, VirtualAlloc se extiende a NtAllocateVirtualMemory, pero eso es un detalle de implementación) (Contraste esto a POSIX, donde la memoria no puede ser reclamado). Este proceso es lento (y IIRC, requiere que se asigne en fragmentos de tamaño de página física, generalmente de 4 kb o más). Windows también proporciona sus propias funciones de montón (HeapAlloc, HeapFree, etc.) como parte de una biblioteca conocida como RtlHeap, que se incluye como parte del propio Windows, sobre el cual el tiempo de ejecución de C (es decir, malloc y amigos) se implementa típicamente .

Windows también tiene bastantes API de asignación de memoria heredada de los días en que tenía que lidiar con los antiguos 80386s, y estas funciones ahora están construidas sobre RtlHeap. Para obtener más información acerca de las diversas API que controlan la administración de memoria en Windows, consulte este artículo de MSDN: http://msdn.microsoft.com/en-us/library/ms810627 .

Tenga en cuenta también que esto significa que en Windows un único proceso (y generalmente lo tiene) tiene más de un montón. (Por lo general, cada biblioteca compartida crea su propio montón).

(La mayoría de esta información proviene de "Codificación segura en C y C ++" por Robert Seacord)


Sdaz ha obtenido una notable cantidad de votos positivos en muy poco tiempo, pero lamentablemente está perpetuando un concepto erróneo sobre cómo las instrucciones se mueven a través de la CPU.

La pregunta hecha

Las instrucciones van desde la RAM, a la pila, a los registros?

Sdaz dijo:

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 se copian (generalmente) desde el área TEXT a la tubería de la CPU, luego a los registros de la CPU y se ejecutan desde allí.

Pero esto está mal. Excepto por el caso especial del código de auto modificación, las instrucciones nunca ingresan a la ruta de datos. Y no se pueden ejecutar desde la ruta de datos.

Los registros de CPU x86 son:

  • Registros generales EAX EBX ECX EDX

  • El segmento registra CS DS ES FS GS SS

  • Índice y sugerencias ESI EDI EBP EIP ESP

  • Indicador EFLAGS

También hay algunos registros de coma flotante y SIMD, pero a los fines de esta discusión los clasificaremos como parte del coprocesador y no como CPU. La unidad de administración de memoria dentro de la CPU también tiene algunos registros propios, nuevamente lo trataremos como una unidad de procesamiento separada.

Ninguno de estos registros se usa para código ejecutable. EIP contiene la dirección de la instrucción de ejecución, no la instrucción en sí misma.

Las instrucciones pasan por una ruta completamente diferente en la CPU a partir de los datos (arquitectura de Harvard). Todas las máquinas actuales son arquitectura de Harvard dentro de la CPU. La mayoría de estos días también son arquitectura de Harvard en el caché. x86 (su máquina de escritorio común) son arquitectura Von Neumann en la memoria principal, lo que significa que los datos y el código se entremezclan en la memoria RAM. Eso no viene al caso, ya que estamos hablando de lo que sucede dentro de la CPU.

La secuencia clásica que se enseña en la arquitectura de la computadora es fetch-decode-execute. El controlador de memoria busca las instrucciones almacenadas en la dirección EIP . Los bits de la instrucción pasan por alguna lógica combinacional para crear todas las señales de control para los diferentes multiplexores en el procesador. Y después de algunos ciclos, la unidad lógica aritmética llega a un resultado, que se sincroniza en el destino. Luego se busca la siguiente instrucción.

En un procesador moderno, las cosas funcionan de manera un poco diferente. Cada instrucción entrante se traduce en una serie completa de instrucciones de microcódigo. Esto permite la canalización, porque los recursos utilizados por la primera microinstrucción no son necesarios más tarde, por lo que pueden comenzar a trabajar en la primera microinstrucción de la siguiente instrucción.

Para colmo, la terminología es un poco confusa porque registrarse es un término de ingeniería eléctrica para una colección de D-flipflops. Y las instrucciones (o especialmente microinstrucciones) pueden almacenarse temporalmente en una colección de D-flipflops. Pero esto no es lo que se quiere decir cuando un informático o un ingeniero de software o un desarrollador común utiliza el término registro . Significan que los registros de ruta de datos se enumeran más arriba, y que no se usan para transportar código.

Los nombres y la cantidad de registros de ruta de datos varían para otras arquitecturas de CPU, como ARM, MIPS, Alpha, PowerPC, pero todas ellas ejecutan instrucciones sin pasarlas por la ALU.


La pila

En el archivo X86, la CPU ejecuta operaciones con registros. La pila solo se usa por razones de conveniencia. Puede guardar el contenido de sus registros para apilar antes de llamar a una subrutina o a una función del sistema y luego cargarlos de nuevo para continuar su operación donde lo dejó. (Podría hacerlo manualmente sin la pila, pero es una función de uso frecuente, por lo que tiene soporte de CPU). Pero puedes hacer casi cualquier cosa sin la pila en una PC.

Por ejemplo, una multiplicación de enteros:

MUL BX

Multiplica el registro AX con el registro BX. (El resultado será en DX y AX, DX que contiene los bits más altos).

Las máquinas basadas en pila (como JAVA VM) usan la pila para sus operaciones básicas. La multiplicación anterior:

DMUL

Esto muestra dos valores desde la parte superior de la pila y multiplica tem, luego empuja el resultado de vuelta a la pila. La pila es esencial para este tipo de máquinas.

Algunos lenguajes de programación de nivel superior (como C y Pascal) utilizan este último método para pasar parámetros a las funciones: los parámetros se envían a la pila en orden de izquierda a derecha y aparecen por el cuerpo de la función y los valores de retorno se devuelven. (Esta es una opción que los fabricantes de compiladores hacen y que tipo de abusos usa el stack de la X86).

El montón

El montón es otro concepto que existe solo en el ámbito de los compiladores. Se lleva el dolor de manejar la memoria detrás de sus variables, pero no es una función de la CPU o el sistema operativo, es solo una opción de limpieza del bloque de memoria que se entrega por el sistema operativo. Puedes hacer esto muchas veces si quieres.

Accediendo a los recursos del sistema

El sistema operativo tiene una interfaz pública de cómo puede acceder a sus funciones. En DOS, los parámetros se pasan en los registros de la CPU. Windows usa la pila para pasar parámetros para las funciones del sistema operativo (la API de Windows).