Conheço a teoria geral, mas não consigo me encaixar nos detalhes.
Eu sei que um programa reside na memória secundária de um computador. Quando o programa começa a execução, ele é totalmente copiado para a RAM. Em seguida, o processador recupera algumas instruções (depende do tamanho do barramento) de cada vez, as coloca em registradores e as executa.
Sei também que um programa de computador usa dois tipos de memória: pilha e pilha, que também fazem parte da memória principal do computador. A pilha é usada para memória não dinâmica e o heap para memória dinâmica (por exemplo, tudo relacionado ao new
operador em C ++)
O que não consigo entender é como essas duas coisas se conectam. Em que momento a pilha é usada para a execução das instruções? As instruções vão da RAM, para a pilha, para os registros?
fonte
Respostas:
Realmente depende do sistema, mas os sistemas operacionais modernos com memória virtual tendem a carregar suas imagens de processo e alocar memória da seguinte forma:
Esse é o espaço de endereço do processo geral em muitos sistemas comuns de memória virtual. O "buraco" é o tamanho da sua memória total, menos o espaço ocupado por todas as outras áreas; isso fornece uma grande quantidade de espaço para o heap crescer. Isso também é "virtual", o que significa que é mapeado para a memória real por meio de uma tabela de conversão e pode ser realmente armazenado em qualquer local da memória real. Isso é feito dessa maneira para proteger um processo de acessar a memória de outro processo e fazer com que cada processo pense que está sendo executado em um sistema completo.
Observe que as posições de, por exemplo, a pilha e a pilha podem estar em uma ordem diferente em alguns sistemas (consulte a resposta de Billy O'Neal abaixo para obter mais detalhes sobre o Win32).
Outros sistemas podem ser muito diferentes. O DOS, por exemplo, rodava em modo real , e sua alocação de memória ao executar programas era muito diferente:
Você pode ver que o DOS permitiu acesso direto à memória do sistema operacional, sem proteção, o que significava que os programas de espaço do usuário geralmente podiam acessar ou substituir diretamente o que quisessem.
No espaço de endereçamento do processo, no entanto, os programas tendiam a parecer semelhantes, apenas eram descritos como segmento de código, segmento de dados, heap, segmento de pilha etc., e era mapeado de maneira um pouco diferente. Mas a maioria das áreas gerais ainda estava lá.
Ao carregar o programa e as bibliotecas compartilhadas necessárias na memória e distribuir as partes do programa nas áreas corretas, o sistema operacional começa a executar seu processo onde quer que esteja o método principal e seu programa assume o controle a partir daí, fazendo chamadas do sistema conforme necessário quando precisa deles.
Sistemas diferentes (incorporados, qualquer que seja) podem ter arquiteturas muito diferentes, como sistemas sem pilha, sistemas de arquitetura Harvard (com código e dados sendo mantidos em memória física separada), sistemas que realmente mantêm o BSS na memória somente leitura (inicialmente definida pelo programador), etc. Mas essa é a essência geral.
Você disse:
"Pilha" e "pilha" são apenas conceitos abstratos, em vez de (necessariamente) "tipos" de memória fisicamente distintos.
Uma pilha é apenas uma estrutura de dados que entra e sai primeiro. Na arquitetura x86, ele pode ser endereçado aleatoriamente usando um deslocamento no final, mas as funções mais comuns são PUSH e POP para adicionar e remover itens dele, respectivamente. É comumente usado para variáveis locais de função (o chamado "armazenamento automático"), argumentos de função, endereços de retorno etc. (mais abaixo)
Um "heap" é apenas um apelido para um pedaço de memória que pode ser alocado sob demanda e é endereçado aleatoriamente (ou seja, você pode acessar qualquer local nele diretamente). É comumente usado para estruturas de dados que você aloca em tempo de execução (em C ++, usando
new
anddelete
,malloc
and friends em C, etc).A pilha e a pilha, na arquitetura x86, residem fisicamente na memória do sistema (RAM) e são mapeadas por meio da alocação de memória virtual no espaço de endereço do processo, conforme descrito acima.
Os registradores (ainda em x86) residem fisicamente dentro do processador (em oposição à RAM) e são carregados pelo processador, na área TEXT (e também podem ser carregados de outros lugares na memória ou em outros locais, dependendo das instruções da CPU que são realmente executados). Eles são essencialmente apenas locais de memória no chip muito pequenos e muito rápidos que são usados para vários propósitos diferentes.
O layout do registro é altamente dependente da arquitetura (de fato, os registros, o conjunto de instruções e o layout / design da memória são exatamente o que se entende por "arquitetura") e, portanto, não vou expandi-lo, mas recomendo que você faça um curso de linguagem assembly para entendê-los melhor.
Sua pergunta:
A pilha (em sistemas / idiomas que os possuem e os usa) é mais frequentemente usada assim:
Escreva um programa simples como este e compile-o na montagem (
gcc -S foo.c
se você tiver acesso ao GCC) e dê uma olhada. A montagem é bem fácil de seguir. Você pode ver que a pilha é usada para variáveis locais de função e para chamar funções, armazenando seus argumentos e retornando valores. É também por isso que quando você faz algo como:Todos esses são chamados por sua vez. É literalmente acumular uma pilha de chamadas de função e seus argumentos, executá-las e, em seguida, dispará-las à medida que diminui (ou aumenta;). No entanto, como mencionado acima, a pilha (no x86) realmente reside no espaço da memória do processo (na memória virtual) e, portanto, pode ser manipulada diretamente; não é uma etapa separada durante a execução (ou pelo menos é ortogonal ao processo).
FYI, o acima é a convenção de chamada C , também usada pelo C ++. Outros idiomas / sistemas podem enviar argumentos para a pilha em uma ordem diferente, e alguns idiomas / plataformas nem usam pilhas e o fazem de maneiras diferentes.
Observe também que essas não são linhas reais de execução do código C. O compilador os converteu em instruções de linguagem de máquina no seu executável.
Eles são copiados (geralmente) da área TEXT para o pipeline da CPU, depois para os registradores da CPU e executados a partir daí.[Isso estava incorreto. Veja a correção de Ben Voigt abaixo.]fonte
Sdaz obteve um número notável de upvotes em um tempo muito curto, mas infelizmente está perpetuando um equívoco sobre como as instruções se movem pela CPU.
A pergunta foi feita:
Sdaz disse:
Mas isso está errado. Exceto no caso especial de código de modificação automática, as instruções nunca entram no caminho de dados. E eles não são, não podem ser, executados a partir do caminho de dados.
Os registradores da CPU x86 são:
Registros gerais EAX EBX ECX EDX
Registros de segmentos CS DS ES FS GS SS
Índice e indicadores ESI EDI EBP EIP ESP
Indicador EFLAGS
Existem também alguns registros de ponto flutuante e SIMD, mas, para os propósitos desta discussão, os classificaremos como parte do coprocessador e não da CPU. A unidade de gerenciamento de memória dentro da CPU também possui alguns registros próprios, trataremos novamente como uma unidade de processamento separada.
Nenhum desses registradores é usado para código executável.
EIP
contém o endereço da instrução de execução, não a própria instrução.As instruções passam por um caminho completamente diferente na CPU dos dados (arquitetura Harvard). Todas as máquinas atuais são arquitetura Harvard dentro da CPU. Atualmente, a maioria dos dias também é arquitetura de Harvard no cache. O x86 (sua máquina comum de desktop) é a arquitetura Von Neumann na memória principal, o que significa que dados e código estão misturados na RAM. Isso não vem ao caso, já que estamos falando sobre o que acontece dentro da CPU.
A sequência clássica ensinada na arquitetura de computadores é buscar-decodificar-executar. O controlador de memória consulta as instruções armazenadas no endereço
EIP
. Os bits da instrução passam por alguma lógica combinatória para criar todos os sinais de controle para os diferentes multiplexadores no processador. E após alguns ciclos, a unidade lógica aritmética chega a um resultado, que é sincronizado no destino. Em seguida, a próxima instrução é buscada.Em um processador moderno, as coisas funcionam de maneira um pouco diferente. Cada instrução recebida é traduzida em uma série inteira de instruções de microcódigo. Isso permite o pipelining, porque os recursos usados pela primeira microinstrução não são necessários posteriormente, para que eles possam começar a trabalhar na primeira microinstrução a partir da próxima instrução.
Ainda por cima, a terminologia é um pouco confusa porque registrar é um termo de engenharia elétrica para uma coleção de chinelos D. E instruções (ou especialmente microinstruções) podem muito bem ser armazenadas temporariamente em uma coleção de chinelos D. Mas não é isso que significa quando um cientista da computação ou engenheiro de software ou desenvolvedor comum usa o termo registrar . Eles significam os registros do caminho de dados listados acima, e eles não são usados para transportar código.
Os nomes e o número de registros do caminho de dados variam para outras arquiteturas de CPU, como ARM, MIPS, Alpha, PowerPC, mas todos executam instruções sem passar pela ALU.
fonte
O layout exato da memória enquanto um processo está em execução depende completamente da plataforma que você está usando. Considere o seguinte programa de teste:
No Windows NT (e seus filhos), esse programa geralmente produz:
Nas caixas POSIX, vai dizer:
O modelo de memória UNIX é bastante bem explicado aqui por @Sdaz MacSkibbons, então não vou reiterar isso aqui. Mas esse não é o único modelo de memória. O motivo pelo qual o POSIX requer esse modelo é a chamada do sistema sbrk . Basicamente, em uma caixa POSIX, para obter mais memória, um processo simplesmente instrui o Kernel a mover o divisor entre o "buraco" e o "heap" para a região "buraco". Não há como devolver memória ao sistema operacional, e o próprio sistema operacional não gerencia sua pilha. Sua biblioteca de tempo de execução C deve fornecer isso (via malloc).
Isso também tem implicações para o tipo de código realmente usado nos binários POSIX. As caixas POSIX (quase universalmente) usam o formato de arquivo ELF. Nesse formato, o sistema operacional é responsável pela comunicação entre bibliotecas em diferentes arquivos ELF. Portanto, todas as bibliotecas usam código independente da posição (ou seja, o próprio código pode ser carregado em diferentes endereços de memória e ainda operar), e todas as chamadas entre bibliotecas são passadas por uma tabela de pesquisa para descobrir onde o controle precisa saltar para chamadas de função de biblioteca. Isso adiciona alguma sobrecarga e pode ser explorado se uma das bibliotecas alterar a tabela de pesquisa.
O modelo de memória do Windows é diferente porque o tipo de código usado é diferente. O Windows usa o formato de arquivo PE, que deixa o código no formato dependente da posição. Ou seja, o código depende de onde exatamente na memória virtual o código é carregado. Há um sinalizador na especificação do PE que informa ao sistema operacional onde exatamente na memória a biblioteca ou o executável gostaria de ser mapeado quando o programa é executado. Se um programa ou biblioteca não puder ser carregado em seu endereço preferido, o carregador do Windows deverá refazera biblioteca / executável - basicamente, move o código dependente da posição para apontar para as novas posições - o que não requer tabelas de pesquisa e não pode ser explorado porque não há tabela de pesquisa para substituir. Infelizmente, isso requer uma implementação muito complicada no carregador do Windows e possui um tempo considerável de inicialização, se uma imagem precisar ser refeita novamente. Grandes pacotes de software comercial geralmente modificam suas bibliotecas para iniciar propositadamente em endereços diferentes, a fim de evitar rebarbas; O próprio Windows faz isso com suas próprias bibliotecas (por exemplo, ntdll.dll, kernel32.dll, psapi.dll, etc. - todos têm endereços de início diferentes por padrão)
No Windows, a memória virtual é obtida do sistema por meio de uma chamada para o VirtualAlloc , e é retornada ao sistema via VirtualFree (Ok, tecnicamente, o VirtualAlloc faz o farm para NtAllocateVirtualMemory, mas esse é um detalhe de implementação) (Compare isso com o POSIX, onde a memória não pode ser recuperado). Esse processo é lento (e o IIRC exige que você aloque em pedaços físicos de tamanho de página; geralmente 4kb ou mais). O Windows também fornece suas próprias funções de heap (HeapAlloc, HeapFree etc.) como parte de uma biblioteca conhecida como RtlHeap, incluída como parte do próprio Windows, na qual o tempo de execução C (ou seja,
malloc
amigos) normalmente é implementado.O Windows também possui algumas APIs de alocação de memória herdada desde os dias em que precisou lidar com os antigos 80386s, e essas funções agora são criadas sobre o RtlHeap. Para obter mais informações sobre as várias APIs que controlam o gerenciamento de memória no Windows, consulte este artigo do MSDN: http://msdn.microsoft.com/en-us/library/ms810627 .
Observe também que isso significa no Windows um único processo e (geralmente) possui mais de um heap. (Normalmente, cada biblioteca compartilhada cria sua própria pilha.)
(A maioria dessas informações vem de "Secure Coding in C and C ++", de Robert Seacord)
fonte
A pilha
Na arquitetura X86, a CPU executa operações com registradores. A pilha é usada apenas por razões de conveniência. Você pode salvar o conteúdo de seus registros para empilhar antes de chamar uma sub-rotina ou uma função do sistema e carregá-los novamente para continuar sua operação de onde você saiu. (Você pode fazer isso manualmente sem a pilha, mas é uma função usada com freqüência, por isso tem suporte para CPU). Mas você pode fazer praticamente qualquer coisa sem a pilha em um PC.
Por exemplo, uma multiplicação inteira:
Multiplica o registro AX pelo registro BX. (O resultado estará em DX e AX, DX contendo os bits mais altos).
Máquinas baseadas em pilha (como JAVA VM) usam a pilha para suas operações básicas. A multiplicação acima:
Isso exibe dois valores da parte superior da pilha e multiplica o tempo e empurra o resultado de volta para a pilha. A pilha é essencial para esse tipo de máquina.
Algumas linguagens de programação de nível superior (como C e Pascal) usam esse método posterior para passar parâmetros para funções: os parâmetros são enviados para a pilha na ordem da esquerda para a direita e exibidos pelo corpo da função e os valores retornados são retornados. (Essa é uma escolha que os fabricantes do compilador fazem e meio que abusam da maneira como o X86 usa a pilha).
A pilha
O heap é um outro conceito que existe apenas no domínio dos compiladores. É difícil lidar com a memória por trás de suas variáveis, mas não é uma função da CPU ou do SO, é apenas uma opção de manter o bloco de memória que é fornecido pelo SO. Você poderia fazer isso muitas vezes, se quiser.
Acessando recursos do sistema
O sistema operacional possui uma interface pública como você pode acessar suas funções. Nos parâmetros do DOS são passados nos registros da CPU. O Windows usa a pilha para transmitir parâmetros para funções do SO (a API do Windows).
fonte