En lenguajes de programación como C y C ++, las personas a menudo se refieren a la asignación de memoria estática y dinámica. Entiendo el concepto, pero la frase "Toda la memoria fue asignada (reservada) durante el tiempo de compilación" siempre me confunde.
La compilación, según tengo entendido, convierte el código C / C ++ de alto nivel al lenguaje de máquina y genera un archivo ejecutable. ¿Cómo se "asigna" la memoria en un archivo compilado? ¿No se asigna siempre la memoria en la RAM con todas las cosas de administración de memoria virtual?
¿No es la asignación de memoria por definición un concepto de tiempo de ejecución?
Si hago una variable asignada estáticamente de 1 KB en mi código C / C ++, ¿aumentará eso el tamaño del ejecutable en la misma cantidad?
Esta es una de las páginas donde se usa la frase bajo el encabezado "Asignación estática".
Volver a lo básico: asignación de memoria, un recorrido por el historial
fuente
Respuestas:
La memoria asignada en tiempo de compilación significa que el compilador resuelve en tiempo de compilación donde ciertas cosas se asignarán dentro del mapa de memoria del proceso.
Por ejemplo, considere una matriz global:
El compilador sabe en tiempo de compilación el tamaño de la matriz y el tamaño de una
int
, por lo que conoce el tamaño completo de la matriz en tiempo de compilación. Además, una variable global tiene una duración de almacenamiento estático por defecto: se asigna en el área de memoria estática del espacio de memoria de proceso (sección .data / .bss). Dada esa información, el compilador decide durante la compilación en qué dirección de esa área de memoria estática estará la matriz .Por supuesto que las direcciones de memoria son direcciones virtuales. El programa asume que tiene su propio espacio de memoria completo (desde 0x00000000 hasta 0xFFFFFFFF por ejemplo). Es por eso que el compilador podría hacer suposiciones como "Bien, la matriz estará en la dirección 0x00A33211". En tiempo de ejecución, la MMU y el sistema operativo traducen las direcciones a direcciones reales / de hardware.
Las cosas de almacenamiento estático inicializadas de valor son un poco diferentes. Por ejemplo:
En nuestro primer ejemplo, el compilador solo decidió dónde se asignará la matriz, almacenando esa información en el ejecutable.
En el caso de cosas con valor inicializado, el compilador también inyecta el valor inicial de la matriz en el ejecutable, y agrega código que le dice al cargador del programa que después de la asignación de la matriz al inicio del programa, la matriz se debe llenar con estos valores.
Aquí hay dos ejemplos del ensamblaje generado por el compilador (GCC4.8.1 con destino x86):
Código C ++:
Ensamblaje de salida:
Como puede ver, los valores se inyectan directamente en el ensamblaje. En la matriz
a
, el compilador genera una inicialización cero de 16 bytes, porque el Estándar dice que las cosas almacenadas estáticas deben inicializarse a cero por defecto:Siempre sugiero a las personas que desmonten su código para ver qué hace realmente el compilador con el código C ++. Esto se aplica desde las clases de almacenamiento / duración (como esta pregunta) hasta las optimizaciones avanzadas del compilador. Puede indicarle a su compilador que genere el ensamblado, pero existen herramientas maravillosas para hacerlo en Internet de manera amigable. Mi favorito es GCC Explorer .
fuente
La memoria asignada en tiempo de compilación simplemente significa que no habrá más asignaciones en tiempo de ejecución, no habrá llamadas a malloc, nuevos u otros métodos de asignación dinámica. Tendrá una cantidad fija de uso de memoria incluso si no necesita toda esa memoria todo el tiempo.
La memoria no está en uso antes del tiempo de ejecución, pero el sistema maneja inmediatamente antes de que se inicie su asignación.
Simplemente declarar la estática no aumentará el tamaño de su ejecutable más de unos pocos bytes. Declararlo con un valor inicial que no sea cero lo hará (para mantener ese valor inicial). Por el contrario, el vinculador simplemente agrega esta cantidad de 1 KB al requisito de memoria que el cargador del sistema crea para usted inmediatamente antes de la ejecución.
fuente
static int i[4] = {2 , 3 , 5 ,5 }
, aumentará en tamaño ejecutable en 16 bytes. Dijiste "Simplemente declarar la estática no aumentará el tamaño de tu ejecutable más que unos pocos bytes. Declararlo con un valor inicial que no sea cero" Declararlo con un valor inicial será lo que significa.La memoria asignada en tiempo de compilación significa que cuando cargue el programa, una parte de la memoria se asignará inmediatamente y el tamaño y la posición (relativa) de esta asignación se determinarán en tiempo de compilación.
Esas 3 variables están "asignadas en tiempo de compilación", significa que el compilador calcula su tamaño (que es fijo) en tiempo de compilación. La variable
a
será un desplazamiento en la memoria, digamos, apuntando a la dirección 0,b
apuntará a la dirección 33 yc
a la 34 (suponiendo que no haya optimización de alineación). Entonces, asignar 1Kb de datos estáticos no aumentará el tamaño de su código , ya que solo cambiará un desplazamiento dentro de él. El espacio real se asignará en el momento de la carga. .La asignación de memoria real siempre ocurre en tiempo de ejecución, porque el kernel necesita realizar un seguimiento y actualizar sus estructuras de datos internas (cuánta memoria se asigna para cada proceso, páginas, etc.). La diferencia es que el compilador ya conoce el tamaño de cada dato que va a usar y esto se asigna tan pronto como se ejecuta su programa.
Recuerde también que estamos hablando de direcciones relativas . La dirección real donde se ubicará la variable será diferente. En el momento de la carga, el núcleo reservará algo de memoria para el proceso, digamos en la dirección
x
, y todas las direcciones codificadas contenidas en el archivo ejecutable se incrementarán enx
bytes, de modo que la variablea
en el ejemplo estará en la direcciónx
, b en la direcciónx+33
y pronto.fuente
Agregar variables en la pila que ocupan N bytes no (necesariamente) aumenta el tamaño del contenedor en N bytes. De hecho, agregará solo unos pocos bytes la mayor parte del tiempo.
Comencemos con un ejemplo de cómo agregar 1000 caracteres a su código voluntad aumentar el tamaño de la papelera de forma lineal.
Si el 1k es una cadena, de mil caracteres, que se declara así
y luego
vim your_compiled_bin
lo harías, en realidad podrías ver esa cadena en el contenedor en alguna parte. En ese caso, sí: el ejecutable será 1 k más grande, porque contiene la cadena en su totalidad.Sin embargo, si asigna una matriz de
int
s,char
solong
s en la pila y la asigna en un bucle, algo en esta líneaentonces, no: no aumentará el contenedor ... por
1000*sizeof(int)
Asignación en tiempo de compilación significa lo que ahora ha llegado a entender que significa (según sus comentarios): el contenedor compilado contiene información que el sistema requiere para saber cuánta memoria qué función / bloque necesitará cuando se ejecute, junto con información sobre el tamaño de pila que requiere su aplicación. Eso es lo que el sistema asignará cuando ejecute su bin, y su programa se convierta en un proceso (bueno, la ejecución de su bin es el proceso que ... bueno, entiende lo que digo).
supuesto, no estoy pintando la imagen completa aquí: la papelera contiene información sobre qué tan grande será la pila que la papelera realmente necesitará. Basado en esta información (entre otras cosas), el sistema reservará una porción de memoria, llamada pila, sobre la cual el programa tendrá una especie de reinado libre. El sistema todavía asigna memoria de pila cuando se inicia el proceso (el resultado de la ejecución de su bin). El proceso gestiona la memoria de la pila por ti. Cuando se invoca / ejecuta una función o bucle (cualquier tipo de bloque), las variables locales de ese bloque se envían a la pila y se eliminan (la memoria de la pila se "libera", por así decirlo) para que otras personas las usen. funciones / bloques. Declarando
int some_array[100]
solo agregará unos pocos bytes de información adicional al contenedor, que le indica al sistema que la función X requerirá100*sizeof(int)
+ un poco de espacio adicional de contabilidad.fuente
i
tampoco se "libera". Sii
residiera en la memoria, simplemente sería empujado a la pila, algo que no se libera en ese sentido de la palabra, ignorando esoi
oc
se mantendrá en registros todo el tiempo. Por supuesto, todo esto depende del compilador, lo que significa que no es tan blanco y negro.free()
llamadas, pero la memoria de pila que usaron es gratuita para que otras funciones la usen una vez que la función que enumeré regresa.En muchas plataformas, el compilador consolidará todas las asignaciones globales o estáticas dentro de cada módulo en tres o menos asignaciones consolidadas (una para datos no inicializados (a menudo llamados "bss"), una para datos de escritura inicializados (a menudo llamados "datos" ), y uno para datos constantes ("const")), y todas las asignaciones globales o estáticas de cada tipo dentro de un programa serán consolidadas por el vinculador en una global para cada tipo. Por ejemplo, suponiendo que
int
son cuatro bytes, un módulo tiene lo siguiente como sus únicas asignaciones estáticas:le diría al enlazador que necesitaba 208 bytes para bss, 16 bytes para "datos" y 28 bytes para "const". Además, cualquier referencia a una variable se reemplazaría con un selector de área y desplazamiento, por lo que a, b, c, d, y e, se reemplazarían por bss + 0, const + 0, bss + 4, const + 24, datos +0 o bss + 204, respectivamente.
Cuando se vincula un programa, todas las áreas bss de todos los módulos se concatenan juntas; igualmente los datos y las áreas constantes. Para cada módulo, la dirección de cualquier variable relativa a bss se incrementará en el tamaño de todas las áreas bss de los módulos anteriores (de nuevo, de la misma manera con datos y const). Por lo tanto, cuando se realiza el enlazador, cualquier programa tendrá una asignación bss, una asignación de datos y una asignación constante.
Cuando se carga un programa, generalmente sucederá una de cuatro cosas dependiendo de la plataforma:
El ejecutable indicará cuántos bytes necesita para cada tipo de datos y, para el área de datos inicializada, donde se pueden encontrar los contenidos iniciales. También incluirá una lista de todas las instrucciones que usan una dirección relativa a bss, data o constante. El sistema operativo o el cargador asignarán la cantidad adecuada de espacio para cada área y luego agregarán la dirección inicial de esa área a cada instrucción que lo necesite.
El sistema operativo asignará una porción de memoria para contener los tres tipos de datos y le dará a la aplicación un puntero a esa porción de memoria. Cualquier código que use datos estáticos o globales lo desreferenciará en relación con ese puntero (en muchos casos, el puntero se almacenará en un registro durante la vida útil de una aplicación).
Inicialmente, el sistema operativo no asignará ninguna memoria a la aplicación, excepto lo que contiene su código binario, pero lo primero que hará la aplicación será solicitar una asignación adecuada del sistema operativo, que siempre mantendrá en un registro.
El sistema operativo inicialmente no asignará espacio para la aplicación, pero la aplicación solicitará una asignación adecuada al inicio (como se indicó anteriormente). La aplicación incluirá una lista de instrucciones con direcciones que deben actualizarse para reflejar dónde se asignó la memoria (como con el primer estilo), pero en lugar de tener la aplicación parcheada por el cargador del sistema operativo, la aplicación incluirá suficiente código para parchearse .
Los cuatro enfoques tienen ventajas y desventajas. Sin embargo, en todos los casos, el compilador consolidará un número arbitrario de variables estáticas en un pequeño número fijo de solicitudes de memoria, y el vinculador consolidará todas ellas en un pequeño número de asignaciones consolidadas. Aunque una aplicación tendrá que recibir una porción de memoria del sistema operativo o del cargador, es el compilador y el enlazador los responsables de asignar las piezas individuales de esa porción grande a todas las variables individuales que lo necesitan.
fuente
El núcleo de su pregunta es: "¿Cómo se" asigna "la memoria en un archivo compilado? ¿No se asigna siempre la memoria en la RAM con todas las cosas de administración de memoria virtual? ¿No es la asignación de memoria por definición un concepto de tiempo de ejecución?"
Creo que el problema es que hay dos conceptos diferentes involucrados en la asignación de memoria. Básicamente, la asignación de memoria es el proceso mediante el cual decimos "este elemento de datos se almacena en esta porción específica de memoria". En un sistema informático moderno, esto implica un proceso de dos pasos:
El último proceso es puramente en tiempo de ejecución, pero el primero se puede hacer en tiempo de compilación, si los datos tienen un tamaño conocido y se requiere un número fijo de ellos. Así es básicamente cómo funciona:
El compilador ve un archivo fuente que contiene una línea que se parece un poco a esto:
Produce una salida para el ensamblador que le indica que reserve memoria para la variable 'c'. Esto podría verse así:
Cuando se ejecuta el ensamblador, mantiene un contador que rastrea las compensaciones de cada elemento desde el inicio de un 'segmento' de memoria (o 'sección'). Esto es como las partes de una 'estructura' muy grande que contiene todo en el archivo completo, no tiene memoria real asignada en este momento y podría estar en cualquier lugar. Se observa en una tabla que
_c
tiene un desplazamiento particular (digamos 510 bytes desde el inicio del segmento) y luego incrementa su contador en 4, por lo que la siguiente variable estará en (por ejemplo) 514 bytes. Para cualquier código que necesite la dirección de_c
, solo pone 510 en el archivo de salida y agrega una nota de que la salida necesita la dirección del segmento que contiene_c
más tarde.El vinculador toma todos los archivos de salida del ensamblador y los examina. Determina una dirección para cada segmento para que no se superpongan, y agrega las compensaciones necesarias para que las instrucciones aún se refieran a los elementos de datos correctos. En el caso de la memoria no inicializada como la ocupada por
c
(se le dijo al ensamblador que la memoria no se inicializaría por el hecho de que el compilador la puso en el segmento '.bss', que es un nombre reservado para la memoria no inicializada), incluye un campo de encabezado en su salida que le indica al sistema operativo cuánto necesita ser reservado. Puede ser reubicado (y generalmente lo es), pero generalmente está diseñado para cargarse de manera más eficiente en una dirección de memoria en particular, y el sistema operativo intentará cargarlo en esta dirección. En este punto, tenemos una idea bastante buena de cuál es la dirección virtual que utilizarác
.La dirección física no se determinará en realidad hasta que se ejecute el programa. Sin embargo, desde la perspectiva del programador, la dirección física es realmente irrelevante: nunca descubriremos qué es, porque el sistema operativo generalmente no se molesta en decirle a nadie, puede cambiar con frecuencia (incluso mientras el programa se está ejecutando), y El objetivo principal del sistema operativo es abstraer esto de todos modos.
fuente
Un ejecutable describe qué espacio asignar para las variables estáticas. El sistema realiza esta asignación cuando ejecuta el ejecutable. Por lo tanto, su variable estática de 1kB no aumentará el tamaño del ejecutable con 1kB:
A menos que, por supuesto, especifique un inicializador:
Entonces, además del 'lenguaje de máquina' (es decir, instrucciones de la CPU), un ejecutable contiene una descripción del diseño de memoria requerido.
fuente
La memoria se puede asignar de muchas maneras:
Ahora su pregunta es qué es "memoria asignada en tiempo de compilación". Definitivamente es solo un dicho incorrectamente redactado, que se supone que se refiere a la asignación de segmento binario o la asignación de pila, o en algunos casos incluso a una asignación de montón, pero en ese caso la asignación está oculta a los ojos del programador por una llamada invisible del constructor. O probablemente la persona que dijo que solo quería decir que la memoria no está asignada en el montón, pero que no sabía acerca de las asignaciones de pila o segmento (o no quería entrar en ese tipo de detalles).
Pero en la mayoría de los casos, la persona solo quiere decir que la cantidad de memoria asignada se conoce en tiempo de compilación .
El tamaño binario solo cambiará cuando la memoria esté reservada en el segmento de código o datos de su aplicación.
fuente
.data
y.bss
.Tienes razón. La memoria se asigna (paginado) en el momento de la carga, es decir, cuando el archivo ejecutable se lleva a la memoria (virtual). La memoria también se puede inicializar en ese momento. El compilador solo crea un mapa de memoria. [Por cierto, ¡los espacios de pila y montón también se asignan en el momento de la carga!]
fuente
Creo que necesitas retroceder un poco. Memoria asignada en tiempo de compilación ... ¿Qué puede significar eso? ¿Puede significar que la memoria en chips que aún no se han fabricado, para computadoras que aún no se han diseñado, se está reservando de alguna manera? No. No, viaje en el tiempo, no hay compiladores que puedan manipular el universo.
Por lo tanto, debe significar que el compilador genera instrucciones para asignar esa memoria de alguna manera en tiempo de ejecución. Pero si lo miras desde el ángulo correcto, el compilador genera todas las instrucciones, entonces, ¿cuál puede ser la diferencia? La diferencia es que el compilador decide, y en tiempo de ejecución, su código no puede cambiar ni modificar sus decisiones. Si decidió que necesitaba 50 bytes en tiempo de compilación, en tiempo de ejecución, no puede hacer que decida asignar 60, esa decisión ya se ha tomado.
fuente
Si aprende la programación de ensamblaje, verá que tiene que tallar segmentos para los datos, la pila y el código, etc. El segmento de datos es donde viven sus cadenas y números. El segmento de código es donde vive su código. Estos segmentos están integrados en el programa ejecutable. Por supuesto, el tamaño de la pila también es importante ... ¡no querrás un desbordamiento de pila !
Entonces, si su segmento de datos es de 500 bytes, su programa tiene un área de 500 bytes. Si cambia el segmento de datos a 1500 bytes, el tamaño del programa será 1000 bytes más grande. Los datos se ensamblan en el programa real.
Esto es lo que sucede cuando compila lenguajes de nivel superior. El área de datos real se asigna cuando se compila en un programa ejecutable, lo que aumenta el tamaño del programa. El programa también puede solicitar memoria sobre la marcha, y esta es una memoria dinámica. Puede solicitar memoria de la RAM y la CPU se la dará para que la use, puede soltarla y su recolector de basura la devolverá a la CPU. Incluso puede ser cambiado a un disco duro, si es necesario, por un buen administrador de memoria. Estas características son las que le proporcionan los lenguajes de alto nivel.
fuente
Me gustaría explicar estos conceptos con la ayuda de unos pocos diagramas.
Esto es cierto que la memoria no se puede asignar en tiempo de compilación, seguro. Pero, entonces, ¿qué sucede de hecho en tiempo de compilación?
Aquí viene la explicación. Digamos, por ejemplo, un programa tiene cuatro variables x, y, z y k. Ahora, en tiempo de compilación, simplemente hace un mapa de memoria, donde se determina la ubicación de estas variables entre sí. Este diagrama lo ilustrará mejor.
Ahora imagine, ningún programa se está ejecutando en la memoria. Esto lo muestro con un gran rectángulo vacío.
A continuación, se ejecuta la primera instancia de este programa. Puedes visualizarlo de la siguiente manera. Este es el momento en que realmente se asigna la memoria.
Cuando se ejecuta la segunda instancia de este programa, la memoria se vería de la siguiente manera.
Y el tercero ...
Y así sucesivamente.
Espero que esta visualización explique bien este concepto.
fuente
Hay una muy buena explicación dada en la respuesta aceptada. En caso de que publique el enlace que he encontrado útil. https://www.tenouk.com/ModuleW.html
fuente