#+title: 1 -- Arquitetura de computadores #+author: Blau Araujo #+email: cursos@blauaraujo.com #+options: toc:3 * Objetivos - Compreender os principais componentes de um computador sob o modelo de von Neumann. - Primeiro contato com os registradores da arquitetura x86_64. - Entender a relação entre o hardware e código Assembly. - Criar um primeiro programa em Assembly com uma chamada de sistema. * Modelo de von Neumann Um computador possui: - Unidade de processamento (ALU + Controladora) - Memória (armazenamento de instruções e dados) - Dispositivos de entrada/saída #+begin_example ┌───────────────────────────────────────┐ │ UNIDADE DE PROCESSAMENTO (CPU) │ │ │ │ • Unidade Lógica e Aritmética (ALU) │ │ • Unidade de Controle (CU) │ └───────────────────┬───────────────────┘ │ ┌───────────────────┴───────────────────┐ │ BARRAMENTO │ └───────┬──────────────────────┬────────┘ │ │ ┌───────┴───────┐ ┌────────┴────────┐ │ MEMÓRIA │ │ DISPOSITIVOS DE │ │ │ │ ENTRADA E SAÍDA │ └───────────────┘ └────────┬────────┘ │ ┌────────┴────────┐ │ ARMAZENAMENTO │ └─────────────────┘ #+end_example A arquitetura de Von Neumann é um modelo de hardware no qual a Unidade Central de Processamento (CPU) e a memória utilizam um espaço único de armazenamento para guardar tanto as instruções dos programas quanto os dados. Isso significa que a CPU acessa a mesma memória para buscar instruções e para ler ou gravar dados, utilizando um único barramento para ambas as operações. Resumindo, no modelo de Von Neumann: - Instruções e dados compartilham o mesmo espaço de memória. - A CPU executa o ciclo: busca → decodificação → execução. Em outros modelos da época (como a arquitetura de Harvard): - Instruções e dados armazenados em memórias separadas. - A CPU executa o ciclo: busca → decodificação → execução, podendo buscar dados e instruções em paralelo. ** Influência nas arquiteturas modernas O modelo de Von Neumann influenciou profundamente as arquiteturas modernas de computadores, estabelecendo a base para a maioria dos designs de sistemas computacionais atuais. Algumas das principais influências incluem: - Memória unificada :: A ideia de compartilhar a mesma memória para dados e instruções se manteve como o padrão em muitas arquiteturas modernas, simplificando o design dos sistemas, embora existam variações: como a memória cache, que separa dados e instruções em níveis mais próximos ao processador. - Ciclo de busca, decodificação e execução :: O modelo de Von Neumann introduziu o ciclo básico de execução de instruções, que ainda é fundamental em CPUs modernas. As arquiteturas atuais, como x86 e ARM, seguem esse ciclo de maneira semelhante, embora com otimizações como a /pipelining/, onde múltiplas fases do ciclo podem ser realizadas em paralelo. - Programação e flexibilidade :: O modelo permite que os programas sejam tratados como dados, o que possibilita a criação de sistemas que podem ser facilmente modificados e adaptados. Isso contribui para o desenvolvimento de linguagens de programação de alto nível, sistemas operacionais e aplicativos dinâmicos. - Simplicidade e custo :: O modelo de Von Neumann simplificou o design de computadores ao integrar a memória de instruções e dados. Isso contribuiu para a redução de custos de hardware, tornando os sistemas mais acessíveis e facilitando o desenvolvimento de sistemas comerciais e pessoais. #+begin_quote *Nota:* A /pipelining/ (do conceito de /"linha de montagem"/) é uma técnica de execução paralela em que múltiplas instruções são processadas simultaneamente em diferentes /estágios/ do ciclo de execução para aumentar o desempenho da CPU. Isso difere do conceito geral de /paralelismo/, onde /tarefas completas/, e não estágios do processamento, são executadas simultaneamente. #+end_quote ** Gargalo de Von Neumann O gargalo de Von Neumann é uma limitação de desempenho causada pelo fato de que a CPU e a memória compartilham o mesmo barramento para acessar instruções e dados. Isso significa que: - A CPU não pode buscar uma instrução e acessar dados ao mesmo tempo. - O tempo de espera entre operações aumenta, especialmente em programas que exigem muitos acessos à memória. - A largura de banda do barramento se torna um fator crítico para o desempenho. Esse gargalo levou ao desenvolvimento de soluções como: - Memória cache (para reduzir acessos à RAM) - Execução paralela e /pipelines/ - Arquiteturas modificadas (como o modelo Harvard modificado) #+begin_quote *Nota:* A /arquitetura Harvard modificada/ é uma variação do modelo de Von Neumann que usa memórias separadas internamente (como em caches) para dados e instruções, mantendo um espaço de memória unificado do ponto de vista do programador. #+end_quote * Arquiteturas x86 A arquitetura x86 é uma família de conjuntos de instruções (ISA – /Instruction Set Architecture/) baseada no modelo de Von Neumann e desenvolvida originalmente pela Intel a partir do processador 8086. Todas as CPUs x86 implementam (ou emulam) o conjunto de instruções da Intel 8086 (16 bits), lançada em 1978. Como uma evolução prática do modelo de Von Neumann, a arquitetura x86 implementa otimizações como: - Uso extensivo de memória cache. - Execução fora de ordem (/out-of-order execution/). - /Pipelines/ e paralelismo interno. ** Características - ISA complexa (CISC – /Complex Instruction Set Computing/), com centenas de instruções e modos de endereçamento. - Suporte a múltiplos tamanhos de palavra (16, 32 e 64 bits nas versões modernas). - Registradores de uso geral (AX, BX, CX, etc.) e segmentados (CS, DS, SS..., não utilizados no Linux), herdados de versões mais antigas. - Ampla compatibilidade com versões anteriores (retrocompatibilidade). - Utilizada em desktops, laptops e servidores, sendo a base da maioria dos PCs atuais. #+begin_quote *Nota:* Embora a arquitetura x86 inclua registradores segmentados (como CS, DS, ES, SS), o Linux não faz uso da segmentação de memória no modo protegido. Em vez disso, ele utiliza o chamado /endereçamento plano/, tratando toda a memória como um único espaço contínuo. A segmentação é mantida apenas em um nível mínimo para atender exigências da arquitetura (como troca de contexto e proteção básica), mas a segmentação lógica, como era usada no MS-DOS, é totalmente evitada. #+end_quote ** Gerações da família x86: | Processador | Ano | Registradores | Endereçamento | Modos de operação | Destaques principais | |----------------+------+---------------+--------------------------------+--------------------------+-----------------------------------------------------------------| | 8086 | 1978 | 16 bits | 20 bits (1 MB) | Real mode | Primeiro da linha x86, sem proteção ou multitarefa | | 80286 | 1982 | 16 bits | 24 bits (16 MB) | Real, Protected | Introduziu o modo protegido e privilégio de acesso por segmento | | 80386 | 1985 | 32 bits | 32 bits (4 GB) | Real, Protected, Virtual | Registradores de 32 bits, suporte à paginação, multitarefa | | 80486 | 1989 | 32 bits | 32 bits | Idem 80386 | Pipeline simples, cache L1, primeira versão com FPU integrada | | Pentium | 1993 | 32 bits | 32 bits | Idem 80486 | Pipeline duplo, superscalar, introdução de MMX | | Pentium Pro | 1995 | 32 bits | 36 bits (PAE) | Idem | Execução fora de ordem, cache L2 on-die, suporte a PAE | | x86-64 (AMD64) | 2003 | 64 bits | 48 bits virtuais (até 57 hoje) | Real, Protected, Long | ISA de 64 bits, registradores expandidos, compatível com x86 | A transição da arquitetura x86 de 32 para 64 bits foi liderada pela AMD com a criação da AMD64 em 2003. Essa extensão manteve compatibilidade com o conjunto de instruções x86 original (IA-32), mas adicionou registradores de 64 bits e suporte a endereçamento ampliado. Posteriormente, a Intel adotou essa mesma arquitetura sob o nome Intel 64 (anteriormente chamada EM64T), e o termo x86_64 passou a ser usado de forma genérica para se referir à arquitetura compatível com AMD64. Assim, o AMD64 é a origem técnica da arquitetura x86 de 64 bits utilizada na maioria dos sistemas modernos. ** Comparativo com outras arquiteturas Todas as arquiteturas modernas seguem, em maior ou menor grau, os princípios do modelo de Von Neumann. No entanto, diferem na forma como organizam e executam instruções. Veja o comparativo: *x86 (CISC – /Complex Instruction Set Computing/):* - Conjunto de instruções extenso e complexo. - Instruções de vários tamanhos e com múltiplos modos de endereçamento. - Maior consumo de energia, mas com alta compatibilidade e maior desempenho bruto. - Retrocompatibilidade com código legado. - Comum em PCs, laptops e servidores. *ARM (RISC – /Reduced Instruction Set Computing/)* - Conjunto de instruções reduzido e regular. - Foco em simplicidade, baixa energia e eficiência. - Desempenho por watt muito superior ao x86. - Predominante em dispositivos móveis (smartphones, tablets) e embarcados. - ARM64 (AArch64) é a versão de 64 bits moderna. *RISC-V (RISC e /open source/):* - ISA aberta, modular e extensível. - Sem royalties: qualquer um pode implementar. - Design limpo e simples, adequado para pesquisa, educação e sistemas customizados. - Crescimento em sistemas embarcados e processadores personalizados. *Resumo comparativo:* | Arquitetura | Tipo | Complexidade | Consumo | Uso comum | |-------------+------+--------------+---------+-------------------------------| | x86 | CISC | Alta | Alto | PCs, laptops, servidores | | ARM | RISC | Média | Baixo | Celulares, IoT, Apple M1+ | | RISC-V | RISC | Baixa | Baixo | Pesquisa, sistemas embarcados | * Componentes de uma CPU x86_64 - Unidade de Controle (/Control Unit/ - CU) :: Gerencia o fluxo de dados e as instruções dentro da CPU, coordenando as operações de execução. - Unidade Lógica e Aritmética (ALU – /Arithmetic and Logic Unit/) :: Responsável pela execução de operações aritméticas (soma, subtração, etc.) e lógicas (AND, OR, NOT, etc.). - Registradores de uso geral :: Utilizados para armazenar dados temporários durante a execução de instruções (ex.: RAX, RBX, RCX, etc.). - Registradores de propósito específico :: Como o ponteiro de pilha (RSP), ponteiro de instrução (RIP) e flags (como EFLAGS). - Registradores de segmentos :: Armazenam endereços de segmentos de memória para código (CS), dados (DS) e pilha (SS), além dos registradores de segmentos adicionais para dados (ES, FS e GS). - Cache :: Memória de acesso ultrarrápido usada para armazenar dados frequentemente acessados, visando reduzir o tempo de acesso à memória principal. Normalmente dividida em L1, L2 e, em algumas CPUs, L3. - Barramentos :: Conjunto de trilhas de comunicação que transportam dados entre a CPU e outros componentes, como a memória, dispositivos de entrada/saída e outros núcleos. - Unidade de Execução (/Execution Unit/ – EU) :: Responsável por executar as instruções. Em CPUs modernas, pode haver múltiplas unidades de execução para executar diferentes tipos de operações em paralelo. - Decodificador de Instruções :: Interpreta as instruções da linguagem de máquina (bytecode) e as converte para operações que podem ser executadas pela ALU ou outras unidades de execução. - Unidade de carga e armazenamento (/Load-Store Unit/ - LSU) :: Controla o carregamento e armazenamento de dados na memória, realizando operações de leitura e escrita. - Unidade de Endereçamento (/Address Generation Unit/ - AGU) :: Calcula endereços de memória, especialmente no contexto de operações de acesso à memória e cálculo de ponteiros. - Controlador de Interrupções (/Interrupt Controller/) :: Responsável por lidar com interrupções externas e internas, gerenciando a prioridade e o tratamento adequado das interrupções no sistema. * Principais registradores e seus propósitos (64 bits) - =RAX=: acumulador/propósito geral - =RBX=: base/propósito geral - =RCX=: contador/propósito geral - =RDX=: dados/propósito geral - =RSI=: índice de origem - =RDI=: índice de destino - =RSP=: ponteiro da pilha - =RBP=: base da pilha - =RIP=: ponteiro de instrução - =RFLAGS=: sinalizações diversas A arquitetura x86_64 ainda inclui oito registradores de propósito geral, de =R8= a =R15=. ** Relação com outras arquiteturas x86 Cada um dos registradores de 64 bits pode ser dividido em partes que, de certo modo, correspondem às capacidades de 32 e 16 bits de outras arquiteturas da família x86. Tomando o registrador =RAX= como exemplo: #+begin_example ┌────────────────────────────────────────────────────┐ │ RAX │ 64 bits └────────────────────────┬───────────────────────────┤ │ EAX │ 32 bits └────────────┬──────────────┤ │ AX │ 16 bits ├───────┬──────┤ │ AH │ AL │ └───────┴──────┘ #+end_example #+begin_quote *Importante:* O diagrama não mostra registradores diferentes, mas os nomes pelos quais as subdivisões do registrador podem ser acessadas! #+end_quote Aplicando a mesma ideia aos principais registradores, os nomes de suas subdivisões seriam: | 64 bits | 32 bits | 16 bits | 8 bits "altos" | 8 bits "baixos" | |---------+---------+---------+----------------+-----------------| | RAX | EAX | AX | AH | AL | | RBX | EBX | BX | BH | BL | | RCX | ECX | CX | CH | CL | | RDX | EDX | DX | DH | DL | | RSI | ESI | SI | (sem acesso) | SIL | | RDI | EDI | DI | (sem acesso) | DIL | | RSP | ESP | SP | (sem acesso) | SPL | | RBP | EBP | BP | (sem acesso) | BPL | | RIP | EIP | IP | (sem acesso) | (sem acesso) | | R8 | R8D | R8W | (sem acesso) | R8B | | ... | ... | ... | ... | ... | | R15 | R15D | R15W | (sem acesso) | R15B | #+begin_quote *Nota:* Assim como os registradores de R8 a R15, nem todas as subdivisões existiam nas arquiteturas de 32 e 16 bits da família x86, ou seja, elas só foram introduzidas na arquitetura x86_64. #+end_quote * Primeiro exemplo em Assembly x86_64 Apenas para apresentar a aparência de um código em Assembly, aqui está o código de um programa que não faz nada além de terminar com o estado de término =42=... Arquivo [[exemplos/01/exit42.asm][exit42.asm]] #+begin_src asm :tangle exemplos/01/exit42.asm ; Retorna 42 como estado de término section .text global _start _start: mov rax, 60 ; syscall: exit mov rdi, 42 ; código de saída syscall #+end_src O programa é pequeno, mas nos dá a oportunidade de conhecer muitos dos elementos de um programa escrito com a linguagem Assembly. ** Seção do código executável #+begin_src asm section .text #+end_src A diretiva =section= não é uma instrução da CPU, mas uma orientação sobre como o binário do programa deverá ser montado. No caso do exemplo, estamos dizendo ao montador que tudo que vier depois de =section=, até que se encontre outra definição de seção, deve ser montado na seção =.text= do binário. O nome =.text= é uma convenção que se refere à seção das instruções do programa em si (o seu código, essencialmente). ** O ponto de entrada #+begin_src asm global _start #+end_src A diretiva =global= diz ao montador que um determinado /rótulo/ (como =_start=) deve ser visível fora do arquivo em que é escrito. Em outras palavras, o símbolo marcado com =global= pode ser referenciado por outros arquivos durante o processo de link-edição. No exemplo, =_start= é o rótulo que representa, por padrão, o endereço da primeira instrução a ser executada em um programa ou, como se costuma dizer, o seu /ponto de entrada/. ** Chamada de sistema Nem todo programa em Assembly fará chamadas de sistema, que são funções internas do kernel para diversas finalidades que envolvam o acesso aos recursos do hardware ou controlados pelo sistema. No exemplo, porém, nós queremos utilizar a chamada de sistema =exit= para terminar a execução do programa retornando o valor =42= para o sistema operacional. Para isso, nós temos que seguir as convenções de chamada, definidas pelo kernel para a arquitetura x86_64. Essas convenções estabelecem, por exemplo, quais registradores devem ser utilizados para receber a identificação da chamada de sistema e os argumentos que serão passados para ela. No caso da chamada de sistema =exit=: - O registrador =rax= deve receber o valor =60= (identificação da chamada =exit=). - O registrador =rdi= deve receber o valor que será retornado como estado de término (=42=). Sendo assim, nós utilizamos as instruções =mov= para carregar (mover) os valores esperados (argumentos) nos registradores apropriados: #+begin_src asm mov rax, 60 ; syscall: exit mov rdi, 42 ; código de saída #+end_src Em seguida, nós utilizamos a instrução =syscall= para informar à CPU que ela deveria invocar a chamada de sistema definida: #+begin_src asm syscall #+end_src ** Montagem e execução (no terminal) #+begin_example :~$ nasm -f elf64 -o exit42.o exit42.asm #+end_example O resultado desse comando é a geração de um arquivo binário que nós chamamos de /objeto/ (com terminação =.o=). O arquivo objeto contém a tradução de todo o código em Assembly para código de máquina e já poderia, por exemplo, ser compilado com outros objetos para compor um programa completo, mas ainda não é capaz de ser executado. Para isso, é necessário submeter o objeto a diversos procedimentos finais chamados de /link-edição/ -- no caso, com o editor de ligações =ld=: #+begin_example :~$ ld -o exit42 exit42.o #+end_example Assim, o editor de ligações =ld=: - Resolve endereços e símbolos (como =_start=); - Organiza o layout final do programa; - Adiciona seções obrigatórias (como os cabeçalhos ELF); - Produz um binário executável. Com o binário final gerado, nós podemos executá-lo e testar seu estado de término com: #+begin_example :~$ ./exit42 :~$ echo $? 42 #+end_example Onde =$?= é a expansão do parâmetro especial do shell =?=, que registra o estado de término do último comando executado. Então, com o valor em =?= expandido, nós podemos imprimi-lo com o comando interno =echo=. * Exercícios sugeridos 1. Antecipe-se e pesquise o que são "chamadas de sistema" (/syscalls/). 2. Modifique o programa Assembly para retornar "sucesso", segundo as convenções do shell do GNU/Linux. 3. Por que eu usei o termo "montagem" para me referir à produção do arquivo binário a partir do código-fonte? 4. Escreva um programa em qualquer linguagem de alto nível que reproduza o funcionamento do exemplo em Assembly. * Referências - [[https://a.co/d/8Dm1Z93][Organização e Projeto de Computadores -- David A. Patterson e John L. Hennesy]] - [[https://a.co/d/9QtcLPI][Organização Estruturada de Computadores -- Andrew S. Tanenbaum]] - [[https://namazso.github.io/x86/][Intel® 64 and IA-32 Instruction Set Reference]] - [[https://wiki.osdev.org/CPU_Registers_x86-64][OS Dev: CPU Registers x86-64]] - [[https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/][Linux System Call Table for x86 64]] - [[https://www.gnu.org/software/bash/manual/bash.html#Exit-Status][Manual do Bash: Estado de Saída]] - =man 2 exit=