O que acontece quando um programa de computador é executado?

Eu conheço a teoria geral, mas não posso me encheckboxr nos detalhes.

Eu sei que um programa reside na memory secundária de um computador. Uma vez que 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, coloca-as em registradores e as executa.

Eu também sei que um programa de computador usa dois tipos de memory: pilha e heap, que também fazem parte da memory principal do computador. A pilha é usada para memory não dinâmica e o heap para memory 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 ponto a pilha é usada para a execução das instruções? As instruções vão da RAM, para a pilha, para os registros?

Isso realmente depende do sistema, mas sistemas operacionais modernos com memory virtual tendem a carregar suas imagens de processo e alocar memory como esta:

 +---------+ | 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 memory virtual. O “buraco” é o tamanho da sua memory 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”, ou seja, é mapeado para a sua memory real através de uma tabela de conversão e pode ser armazenado em qualquer local na memory real. Isso é feito dessa forma para proteger um processo de acessar a memory de outro processo e fazer com que cada processo ache que está sendo executado em um sistema completo.

Note que as posições de, por exemplo, a pilha e o heap podem estar em uma ordem diferente em alguns sistemas (veja a resposta de Billy O’Neal abaixo para mais detalhes sobre o Win32).

Outros sistemas podem ser muito diferentes. O DOS, por exemplo, funcionava em modo real , e sua alocação de memory ao executar programas parecia 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. eg | 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 permitia access direto à memory do sistema operacional, sem proteção, o que significava que os programas de espaço do usuário geralmente podiam acessar ou sobrescrever diretamente qualquer coisa de que gostassem.

No espaço de endereço do processo, no entanto, os programas tendiam a ser parecidos, apenas eles eram descritos como segmento de código, segmento de dados, heap, segmento de pilha, etc., e foi mapeado de forma um pouco diferente. Mas a maioria das áreas gerais ainda estavam lá.

Ao carregar o programa e as bibliotecas compartilhadas necessárias na memory e distribuir as partes do programa nas áreas corretas, o SO começa a executar seu processo onde quer que seu método principal esteja, e seu programa assume a partir dele, fazendo chamadas ao sistema quando necessário 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 memory física separada), sistemas que realmente mantêm o BSS em memory somente leitura (inicialmente definido pelo programador), etc. Mas esta é a essência geral.


Você disse:

Eu também sei que um programa de computador usa dois tipos de memory: pilha e heap, que também fazem parte da memory principal do computador.

“Pilha” e “pilha” são apenas conceitos abstratos, em vez de (necessariamente) “tipos” fisicamente distintos de memory.

Uma pilha é meramente uma estrutura de dados de input e saída. Na arquitetura x86, ele pode realmente ser endereçado aleatoriamente usando um deslocamento do final, mas as funções mais comuns são PUSH e POP para adicionar e remover itens dele, respectivamente. É comumente usado para variables ​​de function local (chamadas de “armazenamento automático”), argumentos de function, endereços de retorno, etc. (mais abaixo)

Um “heap” é apenas um apelido para um pedaço de memory que pode ser alocado sob demanda e é endereçado aleatoriamente (o que significa que você pode acessar qualquer local diretamente nele). É comumente usado para estruturas de dados que você aloca em tempo de execução (em C ++, usando new e delete , e malloc e friends em C, etc).

A pilha e o heap, na arquitetura x86, residem fisicamente na memory do sistema (RAM) e são mapeados por meio da alocação de memory 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, a partir da área TEXTO (e também podem ser carregados de outro lugar na memory ou em outros locais, dependendo das instruções da CPU). são realmente executados). Eles são essencialmente locais de memory no chip muito pequenos e muito rápidos que são usados ​​para várias finalidades diferentes.

O layout do registro é altamente dependente da arquitetura (na verdade, registradores, conjunto de instruções e layout / design da memory, são exatamente o que se entende por “arquitetura”), e por isso não vou expandi-lo, mas recomendo que você curso de linguagem assembly para entendê-los melhor.


Sua pergunta:

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

A pilha (em sistemas / linguagens que os possuem e usam) é 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 então compile-o para assembly ( gcc -S foo.c se você tiver access ao GCC), e dê uma olhada. A assembly é muito fácil de seguir. Você pode ver que a pilha é usada para variables ​​locais de function e para chamar funções, armazenar seus argumentos e retornar valores. É também por isso que quando você faz algo como:

 f( g( h( i ) ) ); 

Todos estes são chamados por sua vez. É literalmente construir uma pilha de chamadas de function e seus argumentos, executá-los e, em seguida, exibi-los quando ele volta para baixo (ou para cima). No entanto, como mencionado acima, a pilha (em x86) realmente reside em seu espaço de memory de processo (na memory virtual) e, portanto, pode ser manipulado 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 empurrar argumentos para a pilha em uma ordem diferente, e algumas linguagens / plataformas nem mesmo usam pilhas, e o fazem de maneiras diferentes.

Observe também que estas não são linhas reais de execução de código em C. O compilador os converteu em instruções de linguagem de máquina no seu executável. Eles são então (geralmente) copiados 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.]

A 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 através da CPU.

A pergunta feita:

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

Sdaz disse:

Observe também que estas não são linhas reais de execução de código em C. O compilador os converteu em instruções de linguagem de máquina no seu executável. Eles são então (geralmente) copiados da área TEXT para o pipeline da CPU, depois para os registradores da CPU e executados a partir daí.

Mas isso está errado. Exceto pelo caso especial do código de auto-modificação, 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:

  • Registos gerais EAX EBX ECX EDX

  • Registros de segmento CS DS ES FS GS SS

  • Índice e pointers 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 nós os classificaremos como parte do coprocessador e não da CPU. A unidade de gerenciamento de memory dentro da CPU também possui alguns registros próprios, trataremos novamente como uma unidade de processamento separada.

Nenhum desses registros é usado para código executável. EIP contém o endereço da instrução em execução, não a instrução em si.

As instruções passam por um caminho completamente diferente na CPU a partir dos dados (arquitetura de Harvard). Todas as máquinas atuais são arquitetura Harvard dentro da CPU. A maioria destes dias também é arquitetura de Harvard no cache. x86 (sua máquina desktop comum) é a arquitetura Von Neumann na memory principal, o que significa que os dados e o código são misturados na RAM. Isso é irrelevante, já que estamos falando sobre o que acontece dentro da CPU.

A sequência clássica ensinada na arquitetura do computador é a busca e decodificação. O controlador de memory consulta a instrução armazenada no endereço EIP . Os bits da instrução passam por alguma lógica combinacional 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 é cronometrado no destino. Então a próxima instrução é buscada.

Em um processador moderno, as coisas funcionam de maneira um pouco diferente. Cada instrução de input é traduzida em uma série inteira de instruções de microcódigo. Isso permite o pipelining, porque os resources usados ​​pela primeira microinstrução não são necessários mais tarde, 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 o registro é um termo de engenharia elétrica para uma coleção de D-flip-flops. E instruções (ou especialmente microinstruções) podem muito bem ser armazenadas temporariamente em uma coleção de D-flipflops. Mas isso não é o que se quer dizer quando um cientista da computação ou engenheiro de software ou um desenvolvedor comum usa o termo registrador . Eles significam os registros do caminho de dados, conforme listado acima, e estes não são usados ​​para transportar código.

Os nomes e número de registradores de datapath variam para outras arquiteturas de CPU, como ARM, MIPS, Alpha, PowerPC, mas todas elas executam instruções sem passá-las através da ALU.

O layout exato da memory enquanto um processo está sendo executado é completamente dependente da plataforma que você está usando. Considere o seguinte programa de teste:

 #include  #include  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 é filho), este programa geralmente produz:

O heap está acima da pilha

Nas checkboxs POSIX, vai dizer:

A pilha está acima do heap

O modelo de memory do UNIX é bem explicado aqui pelo @Sdaz MacSkibbons, então não vou reiterar isso aqui. Mas esse não é o único modelo de memory. A razão pela qual o POSIX requer este modelo é a chamada do sistema sbrk . Basicamente, em uma checkbox POSIX, para obter mais memory, um processo meramente diz ao Kernel para mover o divisor entre o “buraco” e o “heap” para a região do “buraco”. Não há como retornar a memory ao sistema operacional e o próprio sistema operacional não gerencia seu heap. 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 em binários POSIX. Caixas POSIX (quase que universalmente) usam o formato de arquivo ELF. Nesse formato, o sistema operacional é responsável pelas comunicações entre bibliotecas em diferentes arquivos ELF. Portanto, todas as bibliotecas usam código independente de posição (ou seja, o próprio código pode ser carregado em endereços de memory diferentes e ainda operar) e todas as chamadas entre bibliotecas são passadas por uma tabela de pesquisa para descobrir onde o controle precisa ir para cross chamadas de function de biblioteca. Isso adiciona alguma sobrecarga e pode ser explorado se uma das bibliotecas alterar a tabela de consulta.

O modelo de memory 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 memory virtual o código é carregado. Há um sinalizador na especificação PE que informa ao sistema operacional onde exatamente na memory 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 preferencial, o carregador do Windows deve restaurar a biblioteca / executável – basicamente, ele move o código dependente de posição para apontar para as novas posições – o que não requer tabelas de consulta e não pode ser explorado porque não há tabela de pesquisa para replace. Infelizmente, isso requer uma implementação muito complicada no carregador do Windows e uma sobrecarga considerável de tempo de boot se uma imagem precisar ser rebaixada. Os grandes pacotes de software comercial geralmente modificam suas bibliotecas para iniciar intencionalmente em endereços diferentes para evitar rebasing; O próprio Windows faz isso com suas próprias bibliotecas (por exemplo, ntdll.dll, kernel32.dll, psapi.dll, etc. – todos têm diferentes endereços iniciais por padrão)

No Windows, a memory virtual é obtida do sistema através de uma chamada ao VirtualAlloc e é retornada ao sistema via VirtualFree (Ok, tecnicamente VirtualAlloc faz o farms para NtAllocateVirtualMemory, mas isso é um detalhe de implementação) (Contraste isso para POSIX, onde a memory não pode ser recuperado). Esse processo é lento (e o IIRC requer que você aloque em blocos de tamanho de página física, normalmente 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 ( malloc e amigos) é geralmente implementado. .

O Windows também tem algumas APIs de alocação de memory legada dos dias em que precisou lidar com os antigos 80386s, e essas funções agora são construídas sobre o RtlHeap. Para obter mais informações sobre as várias APIs que controlam o gerenciamento de memory no Windows, consulte este artigo do MSDN: http://msdn.microsoft.com/pt-br/library/ms810627 .

Observe também que isso significa no Windows um único processo que (e geralmente possui) tem mais de um heap. (Normalmente, cada biblioteca compartilhada cria seu próprio heap.)

(A maior parte desta informação vem de “Secure Coding in C and C ++” de Robert Seacord)

A pilha

No architercture X86, a CPU executa operações com registradores. A pilha é usada apenas por motivos de conveniência. Você pode salvar o conteúdo de seus registradores para empilhar antes de chamar uma sub-rotina ou uma function do sistema e então carregá-los de volta para continuar sua operação onde você saiu. (Você pode fazer isso manualmente, sem a pilha, mas é uma function 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 de números inteiros:

 MUL BX 

Multiplica o registro AX com o registrador BX. (O resultado será 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 tira dois valores do topo da pilha e multiplica dez, e então empurra o resultado de volta para a pilha. Stack é essencial para este tipo de máquinas.

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 colocados na pilha da esquerda para a direita e estourados pelo corpo da function e os valores de retorno são retornados. (Essa é uma escolha que os fabricantes de compiladores fazem e meio que abusa a maneira como o X86 usa a pilha).

O heap

O heap é outro conceito que existe apenas no domínio dos compiladores. Leva a dor de lidar com a memory por trás de suas variables ​​de distância, mas não é uma function da CPU ou do sistema operacional, é apenas uma escolha de housekeeping o bloco de memory que é dado pelo sistema operacional. Você poderia fazer isso muitas vezes, se quiser.

Acessando resources do sistema

O sistema operacional tem uma interface pública como você pode acessar suas funções. No DOS os parâmetros são passados ​​nos registradores da CPU. O Windows usa a pilha para passar parâmetros para funções do sistema operacional (a API do Windows).