MAC0329 (2023) --- Aula do dia 06/07 ------------------------------------ Na aula anterior vimos a organização geral do modelo de Von Neumann. Consiste de três componentes: - CPU - RAM - interfaces de I/O (entrada/saída) A CPU, por sua vez, é composta de - ULA (Unidade lógico-aritmética) - Registradores - UC (Unidade de controle) Essas componentes estão ligadas por meio de "buses" (barramentos) de dados, endereços e/ou sinais. A unidade de controle de um processador está preparada para decodificar instruções de máquina pré-definidas. Decodificar uma instrução pode ser entendida como "preparar os sinais de controle no processador de forma que os endereços de memória a serem acessados estejam corretos e o tráfego de dados no barramento de dados esteja direcionado para o lado correto, antes da execução da instrução". Um programa de computador é, no nível dos circuitos, uma sequência de instruções de máquina. Para ele ser executado, as instruções devem estar na RAM. O registrador geralmente denotado por PC (program counter) é aquele que contém o endereço da posição da RAM na qual está a próxima instrução a ser executada. O funcionamento do processador é baseado em um sinal de clock. A cada pulso no sinal de clock, ocorre uma mudança de estado (i.e., valores armazenados em componentes do tipo memória podem ser atualizados). Executar instruções corresponde a executar um ciclo de instrução para cada instrução. O ciclo de instrução (FDX) consiste de 3 passos: 1. FETCH: instrução apontada pelo PC é copiado da RAM para o IR e valor do PC é incrementado em 1 2. DECODE: A instrução do IR é decodificada 3. EXECUTE: A instrução é executada. Estado é resetado à configuração de início de ciclo. Esse ciclo é executado sucessivamente para cada uma das instruções do programa, em ordem sequencial (podendo ocorrer DESVIOS, que permitem pular algumas instruções ou voltar e repetir um trecho do programa) --------------------------------------------------------- Uma instrução simples é, por exemplo, Copie o conteúdo do endereço 10 para AC Suponha que essa instrução está na posição 02 da RAM e que é a próxima instrução a ser executada. Nesta situação, - o valor de PC é 02 - à porta de endereço da RAM está chegando o conteúdo do PC - o modo de operação da RAM encontra-se em "Leitura" - a saída da RAM está sendo direcionada para o IR Esta é a configuração do início do ciclo de instrução. Um pulso no clock realiza o passo FETCH, pois a saída da RAM que é o conteúdo do endereço 02 está chegando até o IR e com o pulso esse dado (a instrução a ser executada) é carregado no IR. O código da instrução acima, conforme tabela de códigos do EP3, é "0110", no qual 01 é o código da instrução propriamente e 10 é o endereço ao qual a instrução faz referência. Neste mesmo pulso, o valor do PC deve ser incrementado em 1. Tão logo a instrução é carregada no IR, ela fica disponível na saída do IR, e o código da instrução pode ser consumido pelo decodificador de instruções, e caso a instrução envolva um endereço, ese pode ser direcionado para o local adequado. Qual deve ser o ajuste de sinais a ser realizado pela UC? - o endereço 10 (retirado do byte menos significativo) do IR deve ser direcionado para a porta de endereço da RAM. - o modo de operação da RAM deve continuar em "leitura" - a saída da RAM deve ser direcionada para o AC Assim, um segundo pulso do clock (o passo EXECUTE) fará com que o dado que está na posição 10 da RAM, e que está chegando na entrada do AC, seja "carregado" no AC. Neste passo devemos também cuidar de voltar a configuração para a situação de início de ciclo de instrução. Isto é: - o valor de PC é 03 - à porta de endereço da RAM está chegando o conteúdo do PC - o modo de operação da RAM encontra-se em "Leitura" - a saída da RAM está sendo direcionada para o IR ================================================================== Como são as instruções de máquina "reais"? Vamos ter uma noção sobre isso examinando o código assembly de alguns programas simples. O código assembly é praticamente comparável à linguagem de máquina, porém expressa por meio de mnemônicos. prog1.c -------------------- int main() { int x=5; int y=7; return 0; } -------------------- ===> Para compilar prog1.c, usando o gcc: gcc prog1.c -o prog1 O gcc gerará o binário prog1 (se não ocorrer erro de compilação) ===> Para gerar o código assembly, usando o gcc: gcc -S prog1.c O gcc gerará o arquivo prog1.s, contendo o código assembly correspondente ao programa em prog1.c ===> Código assembly gerado: prog1.s -------------------- .file "prog1.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $5, -8(%rbp) movl $7, -4(%rbp) movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits -------------------- ===> Há uma relação quase direta entre o código assembly e a linguagem de máquina O assembly, por ser simbólico, é mais fácil de ser interpretado por nós (humanos) do que a linguagem de máquina. Mas, mesmo assim, o que está acima é totalmente ininteligível, a não ser que tenhamos conhecimento mínimo sobre a organização dos computadores, sobre como um processador funciona, e sobre assembly. A parte que corresponde a int x=5; int y=7; é movl $5, -8(%rbp) movl $7, -4(%rbp) ===> mantendo apenas as partes relevantes para a compreensão: main: pushq %rbp movq %rsp, %rbp movl $5, -8(%rbp) movl $7, -4(%rbp) movl $0, %eax popq %rbp ret ===> No assembly acima %rbp é o registrador de base da pilha de execução (base point) %rsp é o registrador de topo da pilha de execução (stack point) %eax é um registrador de dados genérico (no caso, usado para armazenar o valor retornado pela função) ===> Dinâmica de execução de uma função A cada execução de uma função (incluindo o programa principal) está associada uma pilha de execução. Uma pilha de execução é um trecho na memória do computador identificado por um BP (base point) e um SP (stack point), isto é, base e topo da pilha. Todas as variáveis locais à função são alocadas (isto é, empilhadas) nessa pilha, e acessadas por meio de deslocamentos relativos ao BP. Por exemplo, se empilhei x e depois y, é bem possível que x esteja na posição BP-4 e y na posição BP-8 (supondo que x e y ocupam 4 bytes). [ o negativo em -4, -8 é porque estamos supondo que a pilha de execução cresce do final da memória em direção ao início ] O uso de BP permite que a pilha de execução de um programa esteja, na prática, em qualquer parte da RAM a cada execução. Quando uma função chama outra função, deve-se criar a pilha de execução da nova função. Uma vez que fisicamente a memória do computador é organizada como posições sequenciais, na prática as chamadas sucessivas às novas funções a partir de uma função, acabam resultando em uma pilha de pilhas de execução. Nesse processo, quando o contexto de execução muda da execução de uma função (digamos f1) para a execução de uma outra função (digamos f2), o BP e SP que apontavam para a pilha de execução de f1 devem ser reajustados para apontarem para a pilha de execução de f2. Antes de se fazer esse reajuste, o próprio valor de BP (da f1) é empilhado [ corresponde a guardar um cópia ]. Depois disso, muda-se o contexto para o da execução de f2. Após o término da execução de f2, a pilha de execução de f2 é desfeita e o BP da f1 pode ser restaurado (basta desempilhar o que foi empilhado antes da execução de f2). Com isso, volta-se ao contexto de execução da f1. [ Há mais detalhes sobre o que e como são empilhados os parâmetros da função, os valores de alguns registradores e as variáveis locais, mas não vamos discutir isso aqui. Esses detalhes serão provavelmente ser cobertos em outras disciplinas. ] O processador é totalmente alheio a qual programa ou qual função está sendo executado num dado instante. A única coisa que ele faz é executar o ciclo FDX repetidamente. Numa instrução de chamada de função, entre outras coisas, o valor atual do PC deve ser empilhado, e em seguida alterado para a primeira instrução da função que está sendo chamada. Isso faz com que no próximo ciclo FDX seja executada a primeira instrução da função [e o processador não tem noção alguma dessa mudança de contexto ]. Quando a execução da função termina, além do BP que é restaurado como explicado acima, o PC também é restaurado. (o PC restaurado é justamente a instrução seguinte à da chamada de função que foi mencionada no início desse parágrafo). O que foi descrito acima refere-se à execução de um programa -- que é chamado de processo. Na prática, múltiplos programas podem estar em execução concomitante, tendo uma área da memória RAM alocada para cada processo. Quem gerencia esses processos é o sistema operacional (SO) e vocês terão oportunidade de aprender mais detalhes quando cursarem SO. E assim a vida segue. Resumo resumidíssimo da aula de hoje :-) [ WARNING: os detalhes podem não estar super-precisos ] ===> Extra: o programa em C para cálculo de fatorial: [ versão com uma função; mas não há necessidade ] fat.c -------------------- #include int fat(int k) { int f; f = 1; while (k > 1) { f = f * k ; k = k - 1; } return f ; } int main() { int n, k; printf("Digite um inteiro positivo (n): "); scanf("%d", &n) ; k = 0 ; while (k <= n) { printf(" %d! = %d\n", k, fat(k)); k = k + 1 ; } return 0; } -------------------- Compilar e gerar o assembly dele fica a seu cargo.