diff --git a/curso/aula-01.org b/curso/aula-01.org index cc4671c..26ef5a0 100644 --- a/curso/aula-01.org +++ b/curso/aula-01.org @@ -7,9 +7,9 @@ * Objetivos - Compreender os principais componentes de um computador sob o modelo de von Neumann. -- Reconhecer os registradores da arquitetura x86_64. -- Entender a relação entre hardware e código Assembly. -- Executar o primeiro programa Assembly com uma chamada de sistema. +- 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 @@ -19,22 +19,45 @@ Um computador possui: - Memória (armazenamento de instruções e dados) - Dispositivos de entrada/saída -O modelo de Von Neumann é uma arquitetura de computadores em que a unidade -central de processamento (CPU) e a memória compartilham um único espaço de -armazenamento, tanto para dados quanto para instruções de programa. Isso -significa que o processador acessa a memória de forma sequencial para buscar -dados e instruções, utilizando um único barramento para ambas as operações. +#+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 │ + └─────────────────┘ -No modelo de Von Neumann: +#+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 → executa. +- A CPU executa o ciclo: busca → decodificação → execução. -Como era antes: +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 → executa, mas pode realizar a busca - de dados e instruções em paralelo. +- A CPU executa o ciclo: busca → decodificação → execução, podendo buscar + dados e instruções em paralelo. ** Influência nas arquiteturas modernas @@ -80,9 +103,9 @@ computacionais atuais. Algumas das principais influências incluem: ** 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. +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: @@ -107,23 +130,15 @@ mantendo um espaço de memória unificado do ponto de vista do programador. 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. +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. -Assim como no modelo de Von Neumann: +Como uma evolução prática do modelo de Von Neumann, a arquitetura x86 implementa +otimizações como: -- Dados e instruções compartilham o mesmo espaço de memória. -- A CPU segue o ciclo: busca → decodifica → executa. - -#+begin_quote -*Nota:* 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, AMD64 é a origem técnica da arquitetura x86 de 64 bits utilizada -na maioria dos sistemas modernos. -#+end_quote +- Uso extensivo de memória cache. +- Execução fora de ordem (/out-of-order execution/). +- /Pipelines/ e paralelismo interno. ** Características @@ -136,22 +151,35 @@ na maioria dos sistemas modernos. - 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. +*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 -** Otimizações em relação ao modelo de Von Neumann +** Gerações da família x86: -A arquitetura x86 é uma evolução prática do modelo de Von Neumann, com -otimizações como: +| 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 | -- Uso extensivo de memória cache. -- Execução fora de ordem (/out-of-order execution/). -- /Pipelines/ e paralelismo interno. +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 @@ -268,8 +296,59 @@ instruções. Veja o comparativo: 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 @@ -284,26 +363,124 @@ _start: 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. Modifique o programa Assembly para retornar "sucesso". -1. Por que eu usei o termo "montagem"? -1. Desmonte (=objdump -d=) os binários gerados e compare os códigos de máquina. -1. Use =strace ./exit42= para verificar a chamada de sistema realizada. +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=