O que acontece quando um programa de computador é executado?


180

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 newoperador 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?


43
+1 por fazer uma pergunta fundamental!
precisa saber é o seguinte

21
hmm ... você sabe, eles escrevem livros sobre isso. Deseja realmente estudar essa parte da arquitetura do sistema operacional com a ajuda do SO?
Andrey

1
Eu adicionei um par de tags baseadas na natureza de memória relacionada com a questão, ea referência a C ++, embora eu acho que uma boa resposta também poderia vir de alguém conhecedor em Java ou C #)!
mkelley33

14
Votado e favorito. Eu sempre tive muito medo de perguntar ...
Maxpm 02/03

2
O termo "coloca-os em registros" não está certo. Na maioria dos processadores, os registradores são usados ​​para armazenar valores intermediários, não código executável.

Respostas:


161

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:

+---------+
|  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.
+---------+

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:

+-----------+ 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

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:

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.

"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 newand delete, mallocand 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:

Em que momento a pilha é usada para a execução das instruções? Instruções vão da RAM, para a pilha, para os registros?

A pilha (em sistemas / idiomas que os possuem e os usa) é mais frequentemente usada assim:

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.
}

Escreva um programa simples como este e compile-o na montagem ( gcc -S foo.cse 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:

f( g( h( i ) ) ); 

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.]


4
desculpe, mas uma recomendação bom livro seria uma resposta melhor, IMO
Andrey

13
Sim, "RTFM" é sempre melhor.
Sdaz MacSkibbons

56
@ Andy: talvez você deva mudar esse comentário para "também, você pode querer ler sua recomendação de bom livro ". Entendo que esse tipo de pergunta merece mais investigação, mas sempre que você precisar iniciar um comentário com "desculpe, mas. .. "talvez você deva considerar sinalizar a postagem para atenção do moderador ou, pelo menos, oferecer uma explicação sobre o motivo de sua opinião ser importante para qualquer pessoa.
precisa saber é o seguinte

2
Excelente resposta. Certamente esclareceu algumas coisas para mim!
Maxpm

2
@Mikael: Dependendo da implementação, você pode ter o armazenamento em cache obrigatório. Nesse caso, sempre que os dados são lidos na memória, uma linha de cache inteira é lida e o cache é preenchido. Ou pode ser possível dar ao gerenciador de cache uma dica de que os dados serão necessários apenas uma vez, portanto, copiá-los para o cache não é útil. Isso é para leitura. Para gravação, existem caches de write-back e write-through, que afetam quando os controladores DMA podem ler os dados e, em seguida, existe um host inteiro de protocolos de coerência de cache para lidar com vários processadores, cada um com seu próprio cache. Isso realmente merece o seu próprio P.
Ben Voigt

61

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:

Instruções vão da RAM, para a pilha, para os registros?

Sdaz disse:

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í.

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. EIPconté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.


Obrigado pelo esclarecimento. Eu hesitava em acrescentar isso, já que não estou intimamente familiarizado, mas fiz isso a pedido de outra pessoa.
Sdaz MacSkibbons

s / ARM / RAM / in "significa que dados e código estão misturados no ARM". Certo?
Bjarke Freund-Hansen

@ bjarkef: A primeira vez que sim, mas não a segunda. Eu resolvo isso.
Ben Voigt

17

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:

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

No Windows NT (e seus filhos), esse programa geralmente produz:

A pilha está acima da pilha

Nas caixas POSIX, vai dizer:

A pilha está acima da pilha

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, mallocamigos) 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)


Ótima informação, obrigado! Espero que "user487117" acabe voltando. :-)
Sdaz MacSkibbons

5

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:

MUL BX

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:

DMUL

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).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.