Capitulo 9788575226674

Page 1

Igor Zhirkov

Novatec


Original English language edition published by Manning Publications Co, Copyright © 2017 by Manning Publications. Portuguese-language edition for Brazil copyright © 2018 by Novatec Editora. All rights reserved. Edição original em inglês publicada pela Manning Publications Co, Copyright © 2017 pela Manning Publications. Edição em português para o Brasil copyright © 2018 pela Novatec Editora. Todos os direitos reservados. Copyright © 2018 da Novatec Editora Ltda. Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998. É proibida a reprodução desta obra, mesmo parcial, por qualquer processo, sem prévia autorização, por escrito, do autor e da Editora. Editor: Rubens PratesLIS20180403 Tradução: Lúcia A. Kinoshita Revisão gramatical: Tássia Carvalho Editoração eletrônica: Carolina Kuwabata ISBN: 978-85-7522-667-4 Histórico de impressões: Abril/2018

Primeira edição

Novatec Editora Ltda. Rua Luís Antônio dos Santos 110 02460-000 – São Paulo, SP – Brasil Tel.: +55 11 2959-6529 Email: novatec@novatec.com.br Site: www.novatec.com.br Twitter: twitter.com/novateceditora Facebook: facebook.com/novatec LinkedIn: linkedin.com/in/novatec LIS20180403


capítulo 1

Básico sobre arquitetura de computadores

Este capítulo permitirá que você tenha uma compreensão geral das bases sobre o funcionamento dos computadores. Descreveremos um modelo nuclear de computação, listaremos suas extensões e veremos duas delas com mais detalhes, a saber, os registradores e a pilha de hardware. Este capítulo preparará você para que comece a programar em Assembly no próximo capítulo.

1.1 Arquitetura do núcleo 1.1.1 Modelo de computação O que um programador faz? Um primeiro palpite provavelmente seria “constrói algoritmos e faz a sua implementação”. Portanto, concebemos uma ideia e, em seguida, programamos, e essa é a maneira usual de pensar. Podemos construir um algoritmo que descreva alguma rotina diária, por exemplo, sair para uma caminhada ou fazer compras? A pergunta não parece particularmente difícil, e muitas pessoas apresentarão satisfatoriamente suas soluções a você. No entanto, todas essas soluções serão fundamentalmente distintas. Uma delas lidará com ações como “abrir a porta” ou “pegar a chave”; outra, porém, utilizará “sair de casa” e omitirá os detalhes. A terceira, por outro lado, poderá ir ao extremo e oferecer uma descrição detalhada do movimento de suas mãos e pernas, ou descreverá até mesmo os padrões de suas contrações musculares. O motivo para essas respostas serem tão diferentes está na incompletude da pergunta inicial.

Todas as ideias (incluindo os algoritmos) precisam de uma maneira de serem expressas. Para descrever uma nova noção, utilizamos outras noções mais simples. 24


Capítulo 1 ■ Básico sobre arquitetura de computadores

25

Além disso, queremos evitar círculos viciosos, portanto a explicação seguirá o formato de uma pirâmide. Cada nível de explicação crescerá horizontalmente. Não podemos construir essa pirâmide de modo infinito, pois a explicação deve ser finita; desse modo, paramos no nível das noções básicas e primitivas, que optamos deliberadamente por não expandir mais. Portanto, escolher o que é básico é um requisito fundamental para expressar qualquer noção. Isso significa que a construção de algoritmos será impossível, a menos que tenhamos definido um conjunto de ações básicas que atuará como os blocos de construção. O modelo de computação é um conjunto de operações básicas e seus respectivos custos. Os custos, geralmente números inteiros, são usados para raciocinar sobre a complexidade dos algoritmos por meio do cálculo do custo combinado de todas as suas operações. Não discutiremos a complexidade computacional neste livro. A maioria dos modelos de computação também são máquinas abstratas. Isso significa que esses modelos descrevem um computador hipotético, cujas instruções correspondem às operações básicas do modelo. O outro tipo de modelo, as árvores de decisão, está além do escopo deste livro.

1.1.2 Arquitetura de von Neumann Vamos agora imaginar que estamos vivendo nos anos 1930, quando os computadores atuais ainda não existiam. As pessoas queriam automatizar os cálculos de alguma forma, e diferentes pesquisadores propunham maneiras distintas de chegar a essa automação. Exemplos comuns são o cálculo Lambda de Church ou a máquina de Turing. São máquinas abstratas típicas, que descrevem computadores imaginários. Um tipo de máquina logo se tornou predominante: o computador com a arquitetura de von Neumann. A arquitetura de um computador descreve a funcionalidade, a organização e a implementação de sistemas de computadores. É uma descrição relativamente de alto nível se comparada a um modelo de cálculo, que não omite sequer o menor dos detalhes. A arquitetura de von Neumann tinha duas vantagens essenciais: era robusta (em um mundo em que os componentes eletrônicos eram extremamente instáveis e tinham vida curta) e fácil de programar. De forma resumida, esse é um computador constituído de um processador e um banco de memória, conectados por um barramento (bus) comum. Uma CPU


26

Programação em Baixo Nível

(Central Processing Unit, ou Unidade Central de Processamento) é capaz de executar instruções, buscadas da memória por uma unidade de controle. A ALU (Arithmetic Logic Unit, ou Unidade Lógica e Aritmética) executa os processamentos necessários. A memória também armazena dados. Veja as Figuras 1.1 e 1.2.

Figura 1.1 – Arquitetura de von Neumann – visão geral.

Figura 1.2 – Arquitetura de von Neumann – memória.

A seguir, listamos as principais características dessa arquitetura: • A memória armazena somente bits (uma unidade de informação, isto é, um valor igual a 0 ou 1). • A memória armazena tanto as instruções codificadas quanto os dados sobre os quais as operações serão feitas. Não há nenhuma forma de distinguir dados de código: com efeito, ambos são cadeias de bits. • A memória está organizada em células, que são rotuladas com seus respectivos índices de forma natural (por exemplo, a célula de número 43 vem depois da célula de número 42). Os índices começam em 0. O tamanho das células pode variar (John von Neumann achava que cada bit devia ter seu endereço); os computadores modernos definem um byte (oito bits) como o tamanho de uma célula de memória. Desse modo, o byte de número 0 armazena os oito primeiros bits da memória, e assim sucessivamente.


Capítulo 1 ■ Básico sobre arquitetura de computadores

27

• O programa é constituído de instruções que são buscadas uma após a outra. Sua execução será sequencial, a menos que uma instrução jump especial seja executada. A linguagem Assembly para um dado processador é uma linguagem de programação constituída de mnemônicos para cada possível instrução binária codificada (código de máquina). Ela deixa a programação em códigos de máquina muito mais simples, pois o programador então não precisa memorizar a codificação binária das instruções, apenas seus nomes e os parâmetros. Observe que as instruções podem ter parâmetros de diferentes tamanhos e formatos. Uma arquitetura nem sempre define um conjunto exato de instruções, de modo diferente de um modelo de computação.

Um computador pessoal moderno comum evoluiu a partir de antigos computadores com arquitetura de von Neumann, portanto investigaremos essa evolução e veremos o que distingue um computador moderno do esquema simples que vemos na Figura 1.2.

Nota O estado da memória e os valores dos registradores descrevem totalmente o estado da CPU (do ponto de vista de um programador). Compreender uma instrução significa entender seus efeitos sobre a memória e os registradores.

1.2 Evolução 1.2.1 Desvantagens da arquitetura de von Neumann A arquitetura simples descrita anteriormente tem sérias desvantagens. Em primeiro lugar, essa arquitetura não é nada interativa. Um programador está limitado a uma edição manual da memória e à visualização de seu conteúdo, de alguma forma. No início da era dos computadores, era muito simples, pois os circuitos eram grandes e os bits podiam ser literalmente virados com as próprias mãos. Além do mais, essa arquitetura não é apropriada à multitarefa. Suponha que seu computador estivesse executando uma tarefa muito lenta (por exemplo, controlando uma impressora). Essa tarefa é lenta porque uma impressora é muito mais lenta que a mais lenta das CPUs. A CPU então precisa esperar por uma reação do dispositivo durante um percentual de tempo próximo a 99%, o que seria um desperdício de recursos (isto é, de tempo de CPU).


28

Programação em Baixo Nível

Além do mais, se todos podem executar qualquer tipo de instrução, toda espécie de comportamentos inesperados poderá ocorrer. O propósito de um sistema operacional (SO) é (entre outros) gerenciar os recursos (por exemplo, os dispositivos externos), de modo que as aplicações dos usuários não provoquem caos ao interagir com os mesmos dispositivos de modo concorrente. Por causa disso, gostaríamos de proibir as aplicações de usuários de executar algumas instruções relacionadas à entrada/saída ou ao gerenciamento do sistema. Outro problema está no fato de o desempenho da memória e da CPU diferirem drasticamente. Nos velhos tempos, os computadores não eram apenas mais simples: eram projetados como entidades integradas. A memória, o barramento, as interfaces de rede – tudo era criado pela mesma equipe de engenharia. Cada parte era especializada de modo a ser utilizada nesse modelo específico. Assim, as partes não eram criadas para serem intercambiáveis. Nessas circunstâncias, ninguém tentava criar uma parte capaz de ter um desempenho melhor do que as outras partes, pois isso não poderia possivelmente melhorar o desempenho geral do computador. No entanto, à medida que as arquiteturas passaram a ser mais ou menos estáveis, os desenvolvedores de hardware começaram a trabalhar em diferentes partes dos computadores, de modo independente. Naturalmente, eles tentaram melhorar seus desempenhos visando ao mercado. Contudo, agilizar todas as partes não era fácil nem barato1. Esse é o motivo pelo qual as CPUs logo se tornaram muito mais rápidas que a memória. É possível agilizar a memória escolhendo outros tipos de circuitos subjacentes, mas seria muito mais caro [12]. Quando um sistema é constituído de diferentes partes e suas características de desempenho diferem muito, a parte mais lenta poderá se transformar em um gargalo. Isso significa que, se a parte mais lenta for substituída por uma parte análoga mais rápida, o desempenho geral melhorará significativamente. É nesse ponto que a arquitetura teve de ser intensamente modificada.

1.2.2 Arquitetura Intel 64 Neste livro, descreveremos somente a arquitetura Intel 64.2 A Intel vem desenvolvendo sua principal família de processadores desde os anos 1970. Cada modelo foi desenvolvido visando a preservar a compatibilidade binária 1 Observe a frequência com que as soluções concebidas pelos engenheiros são determinadas por razões econômicas, e não por limitações técnicas. 2 Também conhecida como x86_64 e AMD64.


Capítulo 1 ■ Básico sobre arquitetura de computadores

29

com os modelos mais antigos. Isso significa que mesmo os processadores modernos podem executar códigos escritos e compilados para modelos mais antigos. Isso resulta em um volume enorme de legado. Os processadores são capazes de operar em uma série de modos: modo real, protegido, virtual etc. Se não for explicitamente especificado, descreveremos o funcionamento da CPU no chamado modo longo (long mode), que é o mais recente.

1.2.3 Extensões da arquitetura O Intel 64 incorpora várias extensões da arquitetura de von Neumann. As mais importantes estão listadas a seguir para uma visão geral rápida. Registradores São células de memória colocadas diretamente no chip da CPU. São muito rápidas entre os circuitos, porém são também mais complicadas e caras. Os acessos aos registradores não utilizam o barramento. O tempo de resposta é bem rápido e, em geral, é igual a dois ciclos de CPU. Veja a seção 1.3 “Registradores”. Pilha de hardware Uma pilha, em geral, é uma estrutura de dados. Ela aceita duas operações: push (inserção) de um elemento no topo e pop (remoção) do elemento superior. Uma pilha de hardware implementa essa abstração no topo da memória por meio de instruções especiais e de um registrador que aponta para o último elemento da pilha. Uma pilha é usada não só em processamentos, mas também para armazenar variáveis locais e implementar sequências de chamadas de função nas linguagens de programação. Veja a seção 1.5 “Pilha de hardware”. Interrupções Esse recurso permite alterar a ordem de execução dos programas com base em eventos externos ao próprio programa. Depois que um sinal (externo ou interno) é capturado, a execução de um programa é suspensa, alguns registradores são salvos e a CPU começa a executar uma rotina especial para lidar com a situação. A seguir, apresentaremos situações de exemplo em que uma interrupção ocorre (e uma porção de código apropriada é executada para tratá-la): • um sinal de um dispositivo externo; • divisão por zero; • instrução inválida (quando a CPU falha em reconhecer uma instrução pela sua representação binária); • uma tentativa de executar uma instrução privilegiada em um modo não privilegiado. Veja a seção 6.2 “Interrupções”, que apresenta uma descrição mais detalhada.


30

Programação em Baixo Nível

Anéis de proteção Uma CPU está sempre em um estado que corresponde a um dos chamados anéis de proteção (protection rings). Cada anel define um conjunto de instruções permitidas. O anel zero permite executar qualquer instrução do conjunto completo de instruções da CPU e, desse modo, é o mais privilegiado. O terceiro permite somente as instruções mais seguras. Uma tentativa de executar uma instrução privilegiada resulta em uma interrupção. A maioria das aplicações funciona no terceiro anel para garantir que elas não modifiquem estruturas de dados críticas do sistema (por exemplo, as tabelas de páginas) e não trabalhem com dispositivos externos, ignorando o sistema operacional. Os outros dois anéis (primeiro e segundo) são intermediários, e os sistemas operacionais modernos não os utilizam. Veja a seção 3.2 “Modo protegido”, que apresenta uma descrição mais detalhada. Memória virtual Essa é uma abstração sobre a memória física, que ajuda a distribuí-la entre os programas de uma maneira mais segura e eficiente. Também isola os programas uns dos outros. Veja a seção 4.2 “Motivação”, que apresenta uma descrição mais detalhada. Algumas extensões não são diretamente acessíveis por um programador (por exemplo, caches ou registradores sombra, isto, é shadow registers). Mencionaremos algumas delas também. A Tabela 1.1 sintetiza informações sobre algumas das extensões da arquitetura de von Neumann vistas em computadores modernos. Tabela 1.1 – Arquitetura de von Neumann: extensões modernas Problema

Solução

Nada é possível sem consultar a memória lenta

Registradores, caches

Falta de interatividade Sem suporte para isolamento de código em procedimentos, ou para salvar contexto Multitarefa (multitasking): qualquer programa pode executar qualquer instrução Multitarefa: os programas não estão isolados uns dos outros

Interrupções Pilha de hardware Anéis de proteção Memória virtual

Fontes de informação Nenhum livro deve abordar o conjunto de instruções e a arquitetura do processador de forma completa. Muitos livros tentam incluir informações abrangentes sobre o conjunto de instruções. Eles se tornarão rapidamente desatualizados; além do mais, o livro ficará desnecessariamente sobrecarregado. ■


Capítulo 1 ■ Básico sobre arquitetura de computadores

31

Com frequência, vamos indicar o Intel® 64 and IA-32 Architectures Software Developer’s Manual (Manual do desenvolvedor de software para as arquiteturas Intel 64 e IA-32), disponível online: veja [15]. Obtenha-o agora! Não há vantagens em copiar as descrições das instruções do local “original” em que elas se encontram; será muito mais proveitoso aprender a trabalhar com a fonte de informações. O segundo volume contém o conjunto de instruções completo, além de ter uma tabela de conteúdo muito útil. Por favor, utilize-o sempre para obter informações sobre o conjunto de instruções: não é apenas uma boa prática, mas também uma fonte de informações muito confiável. Observe que muitos recursos educacionais dedicados à linguagem Assembly na internet com frequência estão extremamente desatualizados (pois poucas pessoas programam hoje em dia em Assembly) e não incluem nada sobre o modo 64 bits. As instruções presentes nos modos mais antigos em geral têm suas contrapartidas atualizadas no modo longo, e elas funcionam de um modo diferente. Esse é o motivo pelo qual não recomendamos utilizar ferramentas de pesquisa para encontrar descrições de instruções, por mais tentador que isso pareça.

1.3 Registradores A troca de dados entre CPU e memória é uma parte crítica dos processamentos em um computador de von Neumann. As instruções precisam ser buscadas na memória, os operandos também e algumas instruções igualmente armazenam os resultados na memória. Isso cria um gargalo e resulta em tempo de CPU desperdiçado quando ela espera os dados de resposta do chip de memória. Para evitar esperas constantes, um processador era equipado com suas próprias células de memória, chamadas de registradores. São poucos, porém rápidos. Em geral, os programas são escritos para que, na maior parte do tempo, o conjunto funcional de células de memória seja suficientemente pequeno. Esse fato sugere que os programas podem ser escritos de modo que, na maior parte do tempo, a CPU esteja trabalhando com registradores. Os registradores são baseados em transistores, enquanto a memória principal utiliza condensadores. Poderíamos ter implementado a memória principal com transistores, e teríamos obtido um circuito muito mais rápido. Há várias razões pelas quais os engenheiros preferiram outras formas de agilizar os processamentos:


32

Programação em Baixo Nível

• Os registradores são mais caros. • As instruções codificam o número do registrador como parte de seus códigos. Para endereçar mais registradores, as instruções teriam de ter um tamanho maior. • Os registradores acrescentam complexidade aos circuitos para endereçá-los. Circuitos mais complexos são mais difíceis de agilizar. Não é fácil configurar um arquivo grande de registradores para trabalhar em 5 GHz. Naturalmente o uso de registradores, no pior caso, deixa os computadores mais lentos. Se tudo deve ser buscado e inserido nos registradores antes de os processamentos serem feitos e descarregado depois na memória, onde está a vantagem? Os programas, em geral, são escritos de modo que tenham uma propriedade em particular. Não é o resultado de uma lei da natureza, mas do uso de padrões comuns de programação, como laços, funções e reutilização de dados. Essa propriedade se chama localidade de referência, e há dois tipos principais: temporal e espacial. A localidade temporal significa que acessos a um endereço provavelmente serão próximos no tempo. A localidade espacial significa que, depois de acessar um endereço X, o próximo acesso de memória provavelmente será próximo a X (por exemplo, X − 16 ou X + 28). Essas propriedades não são binárias: você pode escrever um programa que exiba uma localidade mais forte ou mais fraca. Programas típicos utilizam o seguinte padrão: o conjunto de dados de trabalho é pequeno e pode ser mantido nos registradores. Depois de buscar os dados e inseri-los nos registradores uma vez, trabalharemos com eles por um bom tempo e, então, os resultados serão descarregados na memória. Os dados armazenados na memória raramente serão utilizados pelo programa. Caso precisemos trabalhar com esses dados, teremos perda de desempenho porque: • precisamos buscar os dados e inseri-los nos registradores; • se todos os registradores estiverem ocupados com dados que ainda sejam necessários mais tarde, teremos de nos livrar de alguns deles, o que significa salvar seu conteúdo em células de memória alocadas temporariamente.

Nota Eis uma situação comum para um engenheiro: reduzir o desempenho no pior caso para melhorá-lo no caso médio. Funciona com muita frequência, mas é impossível quando desenvolvemos sistemas de tempo real, que impõem restrições


Capítulo 1 ■ Básico sobre arquitetura de computadores

33

ao pior tempo de reação do sistema. Exige-se que sistemas como esse produzam uma reação aos eventos em não mais que um certo período de tempo, portanto reduzir o desempenho no pior caso para melhorá-lo em outros não é uma opção.

1.3.1 Registradores de propósito geral Na maior parte das vezes, um programador trabalhará com registradores de propósito geral. Eles são intercambiáveis e podem ser usados em vários comandos distintos. São registradores de 64 bits, nomeados como r0, r1, …, r15. Os oito primeiros podem receber nomes alternativos, os quais representam o significado que eles ostentam em algumas instruções especiais. Por exemplo, r1 é chamado, de modo alternativo, de rcx, em que c quer dizer “cycle” (ciclo). Há uma instrução loop que utiliza rcx como contador de ciclos, mas não aceita nenhum operando explicitamente. É claro que esse tipo de significado especial de registrador reflete-se na documentação dos comandos correspondentes (por exemplo, como um contador para a instrução loop). A Tabela 1.2 lista todos eles; veja também a Figura 1.3. Tabela 1.2 – Registradores de 64 bits de propósito geral Nome

Alias

r0

rax

r3

rbx

r1

rcx

r2

rdx

r4

rsp

r5

rbp

r6

rsi

r7

rdi

r8

Descrição

Uma espécie de “acumulador”, usado em instruções aritméticas. Por exemplo, uma instrução div é utilizada para dividir dois inteiros. Ela aceita um operando e usa rax implicitamente como o segundo. Depois de executar div rcx, um número grande de 128 bits, armazenado em partes em dois registradores rdx e rax, é dividido por rcx, e o resultado é armazenado novamente em rax. Registrador base. Era usado para endereçamento de base nos primeiros modelos do processador. Usado para ciclos (por exemplo, em loop). Armazena dados durante operações de entrada/saída. Armazena o endereço do elemento do topo da pilha de hardware. Veja a seção 1.5 “Pilha de Hardware”. Base do stack frame. Veja a seção 14.1.2 “Convenção de chamadas”. Índice de origem em comandos de manipulação de strings (como movsd) Índice de destino em comandos de manipulação de strings (como movsd)


34

Programação em Baixo Nível Nome

r9 … r15

Alias

Descrição

Surgiram depois. Usados principalmente para armazenar variáveis temporárias (às vezes, porém, são usados implicitamente, como r10, Não há que salva as flags de CPU quando a instrução syscall é executada. Veja o Capítulo 6 “Interrupções e chamadas de sistema”).

Figura 1.3 – Aproximação do Intel 64: registradores de propósito geral.

Nota De modo diferente da pilha de hardware, que é implementada com base na memória principal, os registradores são um tipo totalmente diferente de memória. Desse modo, eles não têm endereços, como ocorre com as células da memória principal! ■


Capítulo 1 ■ Básico sobre arquitetura de computadores

35

Os nomes alternativos, de fato, são mais comuns por razões históricas. Apresentaremos ambos para referência e daremos uma dica sobre cada um deles. Essas descrições semânticas serão apresentadas para referência; você não precisará memorizá-las neste momento. Em geral, você não deve usar os registradores rsp e rbp por causa de seus significados muito especiais (mais tarde, veremos como eles corrompem a pilha e o stack frame). No entanto, podemos executar operações aritméticas diretamente neles, o que faz com que sejam de propósito geral. A Tabela 1.3 mostra os registradores ordenados pelos nomes, seguindo uma convenção de indexação. Tabela 1.3 – Registradores de 64 bits de propósito geral – diferentes convenções de nomenclatura r0

r1

r2

r3

r4

r5

r6

r7

rax

Rcx

rdx

rbx

rsp

rbp

rsi

rdi

Endereçar parte de um registrador é possível. Para cada registrador, podemos endereçar seus 32 bits menos significativos, os 16 bits menos significativos ou os 8 bits menos significativos. Quando os nomes r0,...,r15 são usados, isso é feito com a adição de um sufixo apropriado ao nome de um registrador: • d para double word – 32 bits menos significativos; • w para word – 16 bits menos significativos; • b para byte – 8 bits menos significativos.

Por exemplo: • r7b é o byte menos significativo do registrador r7; • r3w é constituído dos dois bytes menos significativos de r3; • r0d é constituído dos quatros bytes menos significativos de r0.

Os nomes alternativos também permitem endereçar as partes menores. A Figura 1.4 mostra a decomposição dos registradores de propósito geral maiores em partes menores.


36

Programação em Baixo Nível

Figura 1.4 – Decomposição de rax.

A convenção de nomenclatura para acessar partes de rax, rbx, rcx e rdx segue o mesmo padrão; somente a letra do meio (a para rax) muda. Os outros quatro registradores não permitem acesso aos seus segundos bytes menos significativos (como rax permite com o nome ah). A nomenclatura do byte menos significativo difere um pouco para rsi, rdi, rsp e rbp. • As partes menores de rsi e rdi são sil e dil (veja a Figura 1.5). • As partes menores de rsp e rbp são spl e bpl (veja a Figura 1.6). Na prática, os nomes r0-r7 raramente são vistos. Em geral, os programadores se atêm aos nomes alternativos para os oito primeiros registradores de propósito geral. Isso é feito por motivos tanto semânticos quanto de legado: rsp comporta muito mais informações que r4. Os outros oito registradores (r8-r15) só podem ser nomeados usando uma convenção com indexação.

Figura 1.5 – Decomposição de rsi e de rdi.


Capítulo 1 ■ Básico sobre arquitetura de computadores

37

Figura 1.6 – Decomposição de rsp e de rbp.

Inconsistência em escritas Todas as leituras de registradores menores se comportam de forma óbvia. As escritas nas partes de 32 bits, porém, preenchem os 32 bits mais significativos do registrador completo com bits de sinal. Por exemplo, zerar eax zerará todo o rax; armazenar -1 em eax preencherá os 32 bits mais significativos com uns. Outras escritas (por exemplo, em partes de 16 bits) se comportam conforme esperado: não afetam nenhum dos demais bits. Veja a seção 3.4.2 “CISC e RISC”, que apresenta a explicação. ■

1.3.2 Outros registradores Os outros registradores apresentam significados especiais. Alguns registradores têm importância para todo o sistema e, desse modo, não podem ser modificados, exceto pelo sistema operacional. Um programador tem acesso ao registrador rip. É um registrador de 64 bits, que sempre armazena um endereço da próxima instrução a ser executada. Instruções que fazem ramificação (por exemplo, jmp), com efeito, modificam esse registrador. Assim, sempre que qualquer instrução é executada, rip armazenará o endereço da próxima instrução.

Nota As instruções têm tamanhos diferentes!


38

Programação em Baixo Nível

Outro registrador acessível se chama rflags. Ele armazena flags, que refletem o estado atual do programa – por exemplo, qual foi o resultado da última instrução aritmética: se foi negativo, se um overflow ocorreu etc. Suas partes menores são chamadas de eflags (32 bits) e flags (16 bits).

Questão 1 É hora de fazer uma pesquisa preliminar baseada na documentação [15]. Consulte a seção 3.4.3 do primeiro volume para conhecer o registrador rflags. O que significam as flags CF, AF, ZF, OF, SF? Qual é a diferença entre OF e CF? ■

Além desses registradores básicos, há também registradores usados por instruções que trabalham com números de ponto flutuante ou instruções paralelas especiais, capazes de executar ações similares em vários pares de operandos ao mesmo tempo. Essas instruções são usadas com frequência visando à multimídia (ajudam a agilizar os algoritmos de decodificação de multimídia). Os registradores correspondentes têm 128 bits de tamanho e recebem os nomes xmm0 a xmm15. Eles serão discutidos mais adiante. Alguns registradores surgiram como extensões não padrões, porém, logo depois, foram padronizados. São os chamados registradores específicos de modelo (model-specific registers). Consulte a seção 6.3.1 “Registradores específicos de modelos” para ver mais detalhes.

1.3.3 Registradores de sistema Alguns registradores foram projetados especificamente para serem usados pelo sistema operacional. Esses registradores não armazenam valores usados em processamentos. Em vez disso, armazenam informações necessárias às estruturas de dados utilizadas por todo o sistema. Portanto, sua função é oferecer suporte para um framework resultante de uma simbiose entre o sistema operacional e a CPU. Todas as aplicações executam nesse framework. Ele garante que as aplicações estejam bem isoladas do sistema propriamente dito, e isoladas umas das outras; além disso, ele administra recursos de modo mais ou menos transparente para um programador. É extremamente importante que esses registradores sejam inacessíveis pelas próprias aplicações (no mínimo, as aplicações não devem ser capazes de modificá-los). Esse é o objetivo do modo privilegiado (veja a seção 3.2).


Capítulo 1 ■ Básico sobre arquitetura de computadores

39

Listaremos alguns desses registradores a seguir. Seus significados serão explicados em detalhes mais adiante. • cr0, cr4 armazenam flags relacionadas a diferentes modos do processador e

à memória virtual. • cr2, cr3 são usados para suporte à memória virtual (veja as seções 4.2 “Moti-

vação” e 4.7.1 “Estrutura dos endereços virtuais). • cr8 (cujo alias é tpr) é usado para efetuar um ajuste fino no mecanismo de

interrupções (veja a seção 6.2 “Interrupções”). • efer é outro registrador de flag usado para controlar os modos do processador e

as extensões (por exemplo, modo longo e tratamento de chamadas de sistema). • idtr armazena o endereço da tabela de descritores de interrupção (veja a

seção 6.2 “Interrupções”). • gdtr e ldtr armazenam os endereços das tabelas de descritores (veja a seção

3.2 “Modo protegido”). • cs, ds, ss, es, gs, fs são os chamados registradores de segmento. O mecanismo

de segmentação que oferecem é considerado legado há muitos anos, mas parte dele continua sendo usada para implementar o modo privilegiado. Veja a seção 3.2 “Modo Protegido”.

1.4 Anéis de proteção Os anéis de proteção (protection rings) são um dos mecanismos projetados para limitar a capacidade das aplicações por razões de segurança e de robustez. Foram inventados para o sistema operacional Multics, um precursor direto do Unix. Cada anel corresponde a um determinado nível de privilégio. Cada tipo de instrução está ligado a um ou mais níveis de privilégio, e não é executável em outros níveis. O nível atual de privilégio é armazenado de algum modo (por exemplo, em um registrador especial). O Intel 64 tem quatro níveis de privilégio, dos quais somente dois são usados na prática: o anel 0 (o mais privilegiado) e o anel 3 (o menos privilegiado). Os anéis intermediários foram planejados para serem utilizados para drivers e serviços do sistema operacional, porém os sistemas populares não adotaram essa abordagem.


40

Programação em Baixo Nível

No modo longo, o número do anel de proteção atual é armazenado nos dois bits menos significativos do registrador cs (e duplicados nos bits de ss). Eles só podem ser alterados quando uma interrupção ou uma chamada de sistema forem tratadas. Desse modo, uma aplicação não poderá executar um código arbitrário com níveis elevados de privilégio: ela só poderá chamar um handler de interrupção ou executar uma chamada de sistema. Veja o Capítulo 3 “Legado”, que tem mais informações.

1.5 Pilha de hardware Se estivermos falando de estruturas de dados em geral, uma pilha é uma estrutura de dados, isto é, um contêiner com duas operações: um novo elemento pode ser inserido no topo da pilha (push) e o elemento do topo pode ser retirado da pilha (pop). Há suporte de hardware para uma estrutura de dados desse tipo. Isso não significa que haja também uma memória de pilha separada. É apenas uma espécie de emulação implementada com duas instruções de máquina (push e pop) e um registrador (rsp). O registrador rsp armazena o endereço do elemento que está no topo da pilha. As instruções executam do seguinte modo: • push argumento 1. Conforme o tamanho do argumento (2, 4 e 8 bytes são permitidos), o valor de rsp será decrementado de 2, 4 ou 8. 2. Um argumento é armazenado na memória começando no endereço obtido do rsp modificado. • pop argumento

1. O elemento que está no topo da pilha é copiado para o registrador/ memória. 2. rsp é incrementado com o tamanho de seu argumento. Uma arquitetura expandida está representada na Figura 1.7. A pilha de hardware é mais conveniente para implementar chamadas de função em linguagens de alto nível. Quando uma função A chama outra função B, ela utiliza a pilha para salvar o contexto dos processamentos e retornar a ele depois que B terminar.


Capítulo 1 ■ Básico sobre arquitetura de computadores

41

Figura 1.7 – Intel 64, registradores e pilha.

Eis alguns fatos importantes sobre a pilha de hardware, a maioria dos quais é decorrente de sua descrição: 1. Não há uma situação de pilha vazia, mesmo que não tenhamos executado push nenhuma vez. Um algoritmo pop pode ser executado de qualquer modo, provavelmente devolvendo um elemento do “topo” da pilha contendo lixo. 2. A pilha cresce em direção ao endereço zero. 3. Quase todos os tipos de operando são considerados inteiros com sinal e, desse modo, podem ser expandidos com um bit de sinal. Por exemplo, executar um push com um argumento B916 resultará na seguinte unidade de dados armazenada na pilha: 0xff b9, 0xffffffb9 ou 0xff ff ff ff ff ff ff b9

Por padrão, push utiliza um tamanho de operando de 8 bytes. Portanto, uma instrução push -1 armazenará 0xff ff ff ff ff ff ff ff na pilha.

4. A maioria das arquiteturas que aceitam pilha utiliza o mesmo princípio, com seu topo definido por algum registrador. O que difere, porém, é o significado do respectivo endereço. Em algumas arquiteturas, é o endereço do próximo elemento, que será escrito no próximo push. Em outras, é o endereço do último elemento já inserido na pilha.


42

Programação em Baixo Nível

Trabalhando com a documentação do Intel: como ler as descrições das instruções Abra o segundo volume de [15]. Localize a página correspondente à instrução push. Ela começa com uma tabela. Em nosso caso, analisaremos somente as colunas OPCODE (Código de operação), INSTRUCTION (Instrução), 64 BIT MODE (Modo 64 bits) e DESCRIPTION (Descrição). O campo OPCODE define a codificação de máquina de uma instrução (código de operação). Como podemos ver, há opções e cada opção corresponde a uma diferente DESCRIPTION. Isso significa que, às vezes, não só os operandos variam, mas também os próprios códigos de operação. ■

INSTRUCTION descreve os mnemônicos das instruções e os tipos de operando permitidos. Nesse caso, R representa qualquer registrador de propósito geral, M quer dizer localização na memória, IMM significa valor imediato (por exemplo, uma constante inteira, como 42 ou 1337). Um número define o tamanho do operando. Se somente registradores específicos forem permitidos, esses serão nomeados. Por exemplo: • push r/m16 – push de um registrador de 16 bits de propósito geral ou um número de 16 bits obtido da memória para a pilha. • push CS – push de um registrador de segmento cs. A coluna DESCRIPTION apresenta uma breve explicação dos efeitos da instrução. Em geral, é suficiente para compreendê-la e utilizá-la. • Leia o restante da explicação sobre push. Quando o operando não tem o sinal estendido? • Explique todos os efeitos da instrução push rsp na memória e nos registradores.

1.6 Resumo Neste capítulo, apresentamos uma rápida visão geral da arquitetura de von Neumann. Começamos acrescentando recursos a esse modelo a fim de deixá-lo mais adequado para descrever os processadores modernos. Até agora, vimos os registradores e a pilha de hardware com mais detalhes. O próximo passo será começar a programar em Assembly, e esse será o assunto ao qual o próximo capítulo será dedicado. Veremos alguns programas de exemplo, enfatizaremos vários recursos arquiteturais novos (como endianness e modos de endereçamento) e faremos o design de uma biblioteca simples de entrada/saída para *nix a fim de facilitar a interação com um usuário.


Capítulo 1 ■ Básico sobre arquitetura de computadores

43

Questão 2  Quais são os princípios essenciais da arquitetura de von Neumann?

Questão 3  O que são registradores?

Questão 4  O que é a pilha de hardware?

Questão 5  O que são interrupções?

Questão 6  Quais são os principais problemas que as extensões modernas do modelo de von Neumann tentam resolver?

Questão 7  Quais são os principais registradores de propósito geral do Intel 64?

Questão 8  Qual é o propósito do ponteiro de pilha (stack pointer)?

Questão 9  A pilha pode estar vazia?

Questão 10  É possível contar os elementos em uma pilha?


Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.