MAC0329 (2021) --- Aula do dia 08/07 ===> Exemplo de programa (simples e bobo) em C: 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 executável 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. ===> 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) E há vários outros "palavrões" ===> 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 ] 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. 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 reptidamente. 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). 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.