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 new
and delete
, 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:
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.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:
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.]