Em linguagens de programação como C e C ++, as pessoas geralmente se referem à alocação de memória estática e dinâmica. Entendo o conceito, mas a frase "Toda a memória foi alocada (reservada) durante o tempo de compilação" sempre me confunde.
A compilação, como eu a entendo, converte código C / C ++ de alto nível em linguagem de máquina e gera um arquivo executável. Como a memória é "alocada" em um arquivo compilado? A memória não é sempre alocada na RAM com todo o material de gerenciamento de memória virtual?
A alocação de memória por definição não é um conceito de tempo de execução?
Se eu criar uma variável alocada estaticamente de 1 KB no meu código C / C ++, isso aumentará o tamanho do executável na mesma quantidade?
Esta é uma das páginas em que a frase é usada sob o título "Alocação estática".
De volta ao básico: alocação de memória, uma caminhada pela história
fonte
Respostas:
Memória alocada em tempo de compilação significa que o compilador resolve em tempo de compilação, onde certas coisas serão alocadas dentro do mapa de memória do processo.
Por exemplo, considere uma matriz global:
O compilador sabe em tempo de compilação o tamanho da matriz e o tamanho de um
int
, portanto, conhece todo o tamanho da matriz em tempo de compilação. Além disso, uma variável global tem duração de armazenamento estático por padrão: é alocada na área de memória estática do espaço de memória do processo (seção .data / .bss). Dadas essas informações, o compilador decide durante a compilação em qual endereço da área de memória estática a matriz será .É claro que os endereços de memória são endereços virtuais. O programa assume que ele possui todo o seu próprio espaço de memória (de 0x00000000 a 0xFFFFFFFF, por exemplo). É por isso que o compilador pode fazer suposições como "Ok, a matriz estará no endereço 0x00A33211". No tempo de execução, esses endereços são traduzidos para endereços reais / de hardware pelo MMU e pelo SO.
Valor inicializado armazenamento estático coisas são um pouco diferentes. Por exemplo:
Em nosso primeiro exemplo, o compilador decidiu apenas onde a matriz será alocada, armazenando essas informações no executável.
No caso de itens inicializados por valor, o compilador também injeta o valor inicial da matriz no executável e adiciona código que informa ao carregador do programa que após a alocação da matriz no início do programa, a matriz deve ser preenchida com esses valores.
Aqui estão dois exemplos do assembly gerado pelo compilador (GCC4.8.1 com destino x86):
Código C ++:
Conjunto de saída:
Como você pode ver, os valores são injetados diretamente na montagem. Na matriz
a
, o compilador gera uma inicialização zero de 16 bytes, porque o Padrão diz que itens armazenados estáticos devem ser inicializados como zero por padrão:Eu sempre sugiro que as pessoas desmontem seu código para ver o que o compilador realmente faz com o código C ++. Isso se aplica das classes / duração do armazenamento (como esta pergunta) às otimizações avançadas do compilador. Você pode instruir seu compilador a gerar o assembly, mas existem ferramentas maravilhosas para fazer isso na Internet de maneira amigável. O meu favorito é o GCC Explorer .
fonte
A memória alocada no tempo de compilação significa simplesmente que não haverá alocação adicional no tempo de execução - nenhuma chamada para malloc, novos ou outros métodos de alocação dinâmica. Você terá uma quantidade fixa de uso de memória, mesmo que não precise de toda essa memória o tempo todo.
A memória não está em uso antes do tempo de execução, mas imediatamente antes da execução iniciar sua alocação é tratada pelo sistema.
Simplesmente declarar a estática não aumentará o tamanho do seu executável mais do que alguns bytes. Declará-lo com um valor inicial diferente de zero será (para manter esse valor inicial). Em vez disso, o vinculador simplesmente adiciona essa quantidade de 1 KB ao requisito de memória que o carregador do sistema cria para você imediatamente antes da execução.
fonte
static int i[4] = {2 , 3 , 5 ,5 }
aumentará pelo tamanho do executável em 16 bytes. Você disse "Simplesmente declarar a estática não aumentará o tamanho do seu executável em mais de alguns bytes. Declará-lo com um valor inicial diferente de zero" Declará-lo com valor inicial será o que isso significa.A memória alocada em tempo de compilação significa que, quando você carrega o programa, alguma parte da memória será imediatamente alocada e o tamanho e a posição (relativa) dessa alocação são determinados em tempo de compilação.
Essas 3 variáveis são "alocadas no momento da compilação", significa que o compilador calcula seu tamanho (que é fixo) no momento da compilação. A variável
a
será um deslocamento na memória, digamos, apontando para o endereço 0,b
apontando para o endereço 33 ec
34 (supondo que não haja otimização do alinhamento). Portanto, alocar 1 KB de dados estáticos não aumentará o tamanho do seu código , pois apenas mudará um deslocamento dentro dele. O espaço real será alocado no momento do carregamento .A alocação de memória real sempre acontece em tempo de execução, porque o kernel precisa acompanhá-lo e atualizar suas estruturas de dados internas (quanta memória é alocada para cada processo, páginas e assim por diante). A diferença é que o compilador já sabe o tamanho de cada dado que você vai usar e isso é alocado assim que o programa é executado.
Lembre-se também de que estamos falando de endereços relativos . O endereço real onde a variável será localizada será diferente. No momento do carregamento, o kernel reservará alguma memória para o processo, digamos no endereço
x
, e todos os endereços codificados contidos no arquivo executável serão incrementados emx
bytes, de modo que a variávela
no exemplo esteja no endereçox
, b no endereçox+33
e em breve.fonte
Adicionar variáveis na pilha que ocupam N bytes não aumenta (necessariamente) o tamanho da lixeira em N bytes. Na verdade, ele adicionará apenas alguns bytes na maioria das vezes.
Vamos começar com um exemplo de como a adição de um 1000 caracteres ao seu código irá aumentar o tamanho do bin de uma forma linear.
Se o 1k for uma sequência de mil caracteres, declarada como tal
e você era então
vim your_compiled_bin
, na verdade seria capaz de ver essa corda na lixeira em algum lugar. Nesse caso, sim: o executável será 1 k maior, porque contém a string na íntegra.Se, no entanto, você alocar uma matriz de
int
s,char
s oulong
s na pilha e atribuí-la em um loop, algo nesse sentidoentão, não: não aumentará a lixeira ... por
1000*sizeof(int)
Alocação em tempo de compilação significa o que você agora entende (com base em seus comentários): a lixeira compilada contém informações que o sistema precisa para saber quanta memória qual função / bloco precisará quando for executado, juntamente com informações sobre o tamanho da pilha que seu aplicativo exige. É isso que o sistema alocará quando executar sua lixeira e seu programa se tornar um processo (bem, a execução de sua lixeira é o processo que ... bem, você entendeu o que estou dizendo).
Obviamente, não estou pintando a imagem completa aqui: a lixeira contém informações sobre o tamanho da pilha que a lixeira realmente precisará. Com base nessas informações (entre outras coisas), o sistema reservará um pedaço de memória, chamado pilha, sobre o qual o programa fica livre. A memória da pilha ainda é alocada pelo sistema, quando o processo (o resultado da execução da sua posição) é iniciado. O processo gerencia a memória da pilha para você. Quando uma função ou loop (qualquer tipo de bloco) é invocado / executado, as variáveis locais para esse bloco são enviadas para a pilha e são removidas (a memória da pilha é "liberada", por assim dizer) para ser usada por outros funções / blocos. Então declarando
int some_array[100]
adicionará apenas alguns bytes de informações adicionais à lixeira, informando ao sistema que a função X exigirá100*sizeof(int)
+ algum espaço extra de contabilidade.fonte
i
não é "liberado" ou também. Sei
residisse na memória, seria empurrado para a pilha, algo que não é liberado no sentido da palavra, desconsiderando issoi
ou quec
será mantido em registros o tempo todo. Obviamente, tudo isso depende do compilador, o que significa que não é tão preto e branco.free()
chamadas, mas a memória da pilha que elas usam é gratuita para uso de outras funções quando a função listada retorna. I removido o código, uma vez que pode ser confuso para algunsEm muitas plataformas, todas as alocações globais ou estáticas em cada módulo serão consolidadas pelo compilador em três ou menos alocações consolidadas (uma para dados não inicializados (geralmente chamados de "bss"), outra para dados graváveis inicializados (geralmente denominados "dados" ) e uma para dados constantes ("const")), e todas as alocações globais ou estáticas de cada tipo em um programa serão consolidadas pelo vinculador em uma global para cada tipo. Por exemplo, supondo que
int
sejam quatro bytes, um módulo tem as seguintes alocações estáticas:diria ao vinculador que precisava de 208 bytes para bss, 16 bytes para "dados" e 28 bytes para "const". Além disso, qualquer referência a uma variável seria substituída por um seletor de área e deslocamento, de modo que a, b, c, de ee seriam substituídos por dados bss + 0, const + 0, bss + 4, const + 24, dados +0 ou bss + 204, respectivamente.
Quando um programa é vinculado, todas as áreas bss de todos os módulos são concatenadas juntas; Da mesma forma, as áreas de dados e const. Para cada módulo, o endereço de qualquer variável relativa a bss será aumentado pelo tamanho das áreas de bss de todos os módulos anteriores (novamente, da mesma forma com dados e const). Assim, quando o vinculador estiver concluído, qualquer programa terá uma alocação de bss, uma alocação de dados e uma alocação const.
Quando um programa é carregado, geralmente ocorre uma das quatro coisas, dependendo da plataforma:
O executável indicará quantos bytes ele precisa para cada tipo de dado e - para a área de dados inicializada, onde o conteúdo inicial pode ser encontrado. Também incluirá uma lista de todas as instruções que usam um endereço bss, dados ou constante. O sistema operacional ou o carregador alocará a quantidade apropriada de espaço para cada área e, em seguida, adicionará o endereço inicial dessa área a cada instrução que precisar.
O sistema operacional alocará um pedaço de memória para armazenar todos os três tipos de dados e fornecerá ao aplicativo um ponteiro para esse pedaço de memória. Qualquer código que use dados estáticos ou globais desreferirá a referência desse ponteiro (em muitos casos, o ponteiro será armazenado em um registro durante o tempo de vida de um aplicativo).
O sistema operacional inicialmente não alocará nenhuma memória para o aplicativo, exceto o que contém seu código binário, mas a primeira coisa que o aplicativo faz é solicitar uma alocação adequada do sistema operacional, que permanecerá para sempre em um registro.
O sistema operacional inicialmente não alocará espaço para o aplicativo, mas o aplicativo solicitará uma alocação adequada na inicialização (como acima). O aplicativo incluirá uma lista de instruções com endereços que precisam ser atualizados para refletir onde a memória foi alocada (como no primeiro estilo), mas, em vez de ter o aplicativo corrigido pelo carregador do SO, o aplicativo incluirá código suficiente para se corrigir .
Todas as quatro abordagens têm vantagens e desvantagens. Em todos os casos, no entanto, o compilador consolidará um número arbitrário de variáveis estáticas em um pequeno número fixo de solicitações de memória e o vinculador consolidará todas elas em um pequeno número de alocações consolidadas. Mesmo que um aplicativo precise receber um pedaço de memória do sistema operacional ou do carregador, é o compilador e o vinculador que são responsáveis por alocar partes individuais desse pedaço grande a todas as variáveis individuais que precisam dele.
fonte
O núcleo da sua pergunta é: "Como a memória é" alocada "em um arquivo compilado? A memória não é sempre alocada na RAM com todo o material de gerenciamento de memória virtual? A alocação de memória por definição não é um conceito de tempo de execução?"
Eu acho que o problema é que existem dois conceitos diferentes envolvidos na alocação de memória. Basicamente, a alocação de memória é o processo pelo qual dizemos "esse item de dados é armazenado nesse pedaço específico de memória". Em um sistema de computador moderno, isso envolve um processo de duas etapas:
O último processo é puramente tempo de execução, mas o primeiro pode ser feito em tempo de compilação, se os dados tiverem um tamanho conhecido e um número fixo deles for necessário. Aqui está basicamente como funciona:
O compilador vê um arquivo de origem contendo uma linha que se parece com isso:
Produz saída para o assembler que instrui a reservar memória para a variável 'c'. Isso pode ser assim:
Quando o montador é executado, ele mantém um contador que rastreia as compensações de cada item desde o início de um 'segmento' (ou 'seção') da memória. É como as partes de uma 'estrutura' muito grande que contém tudo no arquivo inteiro; não há memória real alocada para ele no momento e pode estar em qualquer lugar. Ele observa em uma tabela que
_c
possui um deslocamento específico (por exemplo, 510 bytes desde o início do segmento) e depois incrementa seu contador em 4; portanto, a próxima variável será em (por exemplo) 514 bytes. Para qualquer código que precise do endereço de_c
, basta colocar 510 no arquivo de saída e adicionar uma nota de que a saída precisa do endereço do segmento que contém a_c
adição posteriormente.O vinculador pega todos os arquivos de saída do assembler e os examina. Ele determina um endereço para cada segmento para que não se sobreponham e adiciona as compensações necessárias para que as instruções ainda se refiram aos itens de dados corretos. No caso de memória não inicializada como a ocupada por
c
(o montador foi informado de que a memória seria não inicializada pelo fato de o compilador colocá-la no segmento '.bss', que é um nome reservado para a memória não inicializada), inclui um campo de cabeçalho em sua saída que informa ao sistema operacional quanto precisa ser reservado. Pode ser realocado (e geralmente é), mas geralmente é projetado para ser carregado com mais eficiência em um endereço de memória específico, e o sistema operacional tentará carregá-lo nesse endereço. Nesse ponto, temos uma boa idéia de qual endereço virtual será usadoc
.O endereço físico não será realmente determinado até que o programa esteja sendo executado. No entanto, do ponto de vista do programador, o endereço físico é realmente irrelevante - nunca descobriremos o que é, porque o sistema operacional geralmente não se incomoda em contar a ninguém, pode mudar com frequência (mesmo enquanto o programa está em execução) e um O principal objetivo do sistema operacional é abstrair isso de qualquer maneira.
fonte
Um executável descreve qual espaço alocar para variáveis estáticas. Essa alocação é feita pelo sistema, quando você executa o executável. Portanto, sua variável estática de 1kB não aumentará o tamanho do executável com 1kB:
A menos que você especifique um inicializador:
Portanto, além da 'linguagem de máquina' (ou seja, instruções da CPU), um executável contém uma descrição do layout de memória necessário.
fonte
A memória pode ser alocada de várias maneiras:
Agora sua pergunta é o que é "memória alocada em tempo de compilação". Definitivamente, é apenas um ditado incorretamente formulado, que deve se referir à alocação de segmento binário ou alocação de pilha, ou em alguns casos até a alocação de heap, mas nesse caso a alocação está oculta aos olhos do programador por chamada invisível do construtor. Ou provavelmente a pessoa que disse que apenas queria dizer que a memória não está alocada na pilha, mas não sabia sobre alocações de pilha ou segmento (ou não queria entrar nesse tipo de detalhe).
Mas na maioria dos casos, a pessoa só quer dizer que a quantidade de memória que está sendo alocada é conhecida no momento da compilação .
O tamanho binário será alterado apenas quando a memória estiver reservada no segmento de código ou dados do seu aplicativo.
fonte
.data
e.bss
.Você está certo. A memória é realmente alocada (paginada) no momento do carregamento, ou seja, quando o arquivo executável é trazido para a memória (virtual). A memória também pode ser inicializada nesse momento. O compilador apenas cria um mapa de memória. [A propósito, os espaços de pilha e pilha também são alocados no tempo de carregamento!]
fonte
Eu acho que você precisa recuar um pouco. Memória alocada em tempo de compilação .... O que isso significa? Pode significar que a memória em chips que ainda não foram fabricados, para computadores que ainda não foram projetados, está de alguma forma sendo reservada? Não. Não, viagem no tempo, nenhum compilador que possa manipular o universo.
Portanto, isso significa que o compilador gera instruções para alocar essa memória de alguma forma no tempo de execução. Mas se você olhar do ângulo certo, o compilador gera todas as instruções, então qual pode ser a diferença. A diferença é que o compilador decide e, em tempo de execução, seu código não pode alterar ou modificar suas decisões. Se ele decidiu que precisava de 50 bytes em tempo de compilação, em tempo de execução, não é possível decidir alocar 60 - essa decisão já foi tomada.
fonte
Se você aprender a programação de montagem, verá que é necessário esculpir segmentos para os dados, a pilha e o código, etc. O segmento de dados é onde vivem suas seqüências e números. O segmento de código é onde o seu código está localizado. Esses segmentos são incorporados ao programa executável. Claro que o tamanho da pilha também é importante ... você não iria querer um estouro de pilha !
Portanto, se seu segmento de dados tiver 500 bytes, seu programa terá uma área de 500 bytes. Se você alterar o segmento de dados para 1500 bytes, o tamanho do programa será 1000 bytes maior. Os dados são reunidos no programa real.
É o que está acontecendo quando você compila idiomas de nível superior. A área de dados real é alocada quando compilada em um programa executável, aumentando o tamanho do programa. O programa também pode solicitar memória em tempo real, e isso é memória dinâmica. Você pode solicitar memória da RAM e a CPU fornecerá para você usar, você poderá soltá-la e seu coletor de lixo a liberará de volta para a CPU. Pode até ser trocado para um disco rígido, se necessário, por um bom gerenciador de memória. Esses recursos são os idiomas de alto nível que você fornece.
fonte
Eu gostaria de explicar esses conceitos com a ajuda de alguns diagramas.
Isso é verdade que a memória não pode ser alocada em tempo de compilação, com certeza. Mas, o que acontece de fato no momento da compilação.
Aqui vem a explicação. Digamos, por exemplo, um programa tenha quatro variáveis x, y, z e k. Agora, em tempo de compilação, ele simplesmente cria um mapa de memória, onde a localização dessas variáveis entre si é determinada. Este diagrama irá ilustrá-lo melhor.
Agora imagine, nenhum programa está sendo executado na memória. Isso eu mostro por um grande retângulo vazio.
Em seguida, a primeira instância deste programa é executada. Você pode visualizá-lo da seguinte maneira. Este é o momento em que realmente a memória é alocada.
Quando a segunda instância deste programa estiver em execução, a memória terá a seguinte aparência.
E o terceiro ..
E assim por diante.
Espero que essa visualização explique bem esse conceito.
fonte
Há uma explicação muito boa dada na resposta aceita. Apenas no caso eu vou postar o link que eu achei útil. https://www.tenouk.com/ModuleW.html
fonte