Computadores e Programação (DCC/UFRJ) Aula 6:
1 2 3
A necessidade dos programadores escreverem código em linguagem de montagem tem mudado ao longo dos últimos anos: de um programador capaz de escrever programas diretamente em linguagem de montagem para um programador capaz de ler e entender o código gerado pelos compiladores
Compreendendo código em linguagem de montagem Ler código em linguagem de montagem gerado por um compilador envolve um conjunto de habilidades distintas daquelas necessárias para escrever código diretamente: é preciso compreender as transformações usadas pelos compiladores para converter construções da linguagem de alto nível em código de máquina Técnicas de otimização usadas pelos compiladores podem rearranjar a ordem de execução do programa: eliminando computações desnecessárias, substituindo operações lentas, ou mesmo convertendo computações recursivas por sequências iterativas
Linguagem de máquina Intel IA32 Tomaremos como base para nosso estudo as linguagens de máquina Intel IA32 e sua extensão para 64 bits x86-64 Usaremos um subconjunto dessas linguagens, exploradas pelo sistema de compilação GCC e pelo sistema operacional Linux A linha de processadores Intel, referenciada por x86, tem uma longa história de desenvolvimento Produto de vários grupos independentes, com cerca de 30 anos de evolução, adicionando novas características para o conjunto original de instruções
Histórico Início dos microprocessadores na década de 70 Arquitetura de 4 bits com processador 4004, seguida da arquitetura de 8 bits com processador 8008 1974, Intel 8080 (from Wikipedia) Variante estendida do projeto anterior 8008, todavia, sem compatibilidade binária. Clock máximo de 2 Mhz, tecnologia NMOS, exigindo voltagens de +12V e -5V, com instruções exigindo entre 4 a 11 ciclos, executando umas centenas de milhares de instruções por segundo. Primeiro microprocessador verdadeiramente utilizável, apesar dos anteriores terem sido usados em calculadoras e outras aplicações Influenciou decisivamente a arquitetura de 16 bits da série x86.
Histórico 1978 A arquitetura Intel 8086, com todos os registradores internos de 16 bits, foi anunciada como extensão compatível da arquitetura Intel 8080 de 8 bits 1980 O coprocessador de ponto-flutuante Intel 8087 é anunciado Usa a pilha em memória (ao invés de registradores) para as operações de ponto-flutuante e acrescenta 60 instruções
Histórico 1982 Intel 80286 estende a arquitetura 8086 incrementando o espaço de endereçamento para 24 bits 1985 Intel 80386 estende a arquitetura 80286 para 32 bits
Histórico 1989-95 Intel 80486 (1989), Pentium (1992) e Pentium Pro (1995) visam aumento de desempenho, com a adição de poucas instruções visíveis ao usuário 1997 Intel anuncia expansão do Pentium e Pentium Pro com MMX (Multimedia Extensions) 57 novas instruções que operam sobre vários elementos de dados ao mesmo tempo (SIMD)
Histórico 1999 Inclusão do tipo de dados de ponto-flutuante de precisão simples (Pentium III) Adição de 8 registradores 2001 Inclusão do tipo de dados de ponto-flutuante de precisão dupla (Pentium IV)
Histórico 2003 Outra companhia, a AMD, muda todos os registradores para 64 bits (AMD64) 2004 A Intel incorpora o AMD64 2014 Anunciado processador Intel Xeon E7 v2 com 15 núcleos e 1,5 tera bytes de memória (exemplo de processador CISC - complex instruction set computer) para uso em servidores e competindo com processadores RISC (reduced instruction set computer) do PowerPC IBM
Dados dois programas, p1.c e p2.c, eles podem ser compilados fazendo: gcc -O1 -o p p1.c p2.c A opção -O1 diz ao compilador para usar o nível 1 de otimização
Em geral, quanto maior o nível de otimização, mais rápido é a execução do programa final... por outro lado, maior a chance de aumentar o tempo de compilação e dificultar a depuração... além disso, fica mais difícil compreender a relação entre código fonte e final Na prática, o nível -O2 é considerado uma boa opção em termos de desempenho
Código de máquina O sistema de compilação transforma programas expressos no modelo de execução da linguagem de alto nível em instruções elementares que o processador executa A habilidade de entender o código de montagem e sua relação com o código fonte é um passo essencial para compreender como os computadores executam os programas
Exemplo de geração de código Ver exemplo de código em anexo (ex11.c) Fazer gcc -O1 -S ex11.c Ver código em linguagem de montagem gerado em ex11.s (todas as linhas que começam com. são diretivas para o montador e o ligador, podemos em geral ignorá-las)
Exemplo de geração de código A referência à variável global acumulador aparece porque o compilador não determinou ainda em que endereço de memória essa variável será armazenada Os endereços globais só serão preenchidos após a link edição e geração do código executável final As demais declarações de variáveis e tipos não aparecem no arquivo de saída (ex11.s)
Usando um disassembler Fazendo gcc -O1 -c ex11.c geramos o arquivo objeto ex11.o No Linux, podemos usar o programa OBJDUMP para inspecionar o conteúdo de um arquivo objeto ex., objdump -d ex11.o > ex11.elf (ver exemplo em anexo)
Observações sobre o código de máquina gerado As instruções IA32 podem ter de 1 a 15 bytes de tamanho As instruções são codificadas de tal forma que as operações mais comuns requerem um número menor de bytes comparado às demais O formato das instruções é projetado de tal forma que a partir de uma posição inicial há uma única codificação dos bytes em instrução de máquina O disassembler determina o código em linguagem de montagem com base apenas na sequência de bytes do código de máquina e na arquitetura para a qual ele foi gerado (não depende da linguagem fonte do programa original)
Voltando ao exemplo de código... Para gerar o programa executável é preciso um arquivo com a função main Fazendo gcc -O1 -o main ex11.o main.c, temos o arquivo final Fazendo objdump -d main, podemos inspecionar o conteúdo do arquivo final (observar a diferença com relação ao endereço da variável global acumulador )
Observações sobre os diferentes formatos de código de montagem ATT versus Intel O código de montagem IA32 pode ser mostrado em diferentes formatos O formato ATT é o formato padrão das ferramentas gcc e objdump (e será usado ao longo do curso) O formato Intel é usado por outras ferramentas (ex., Microsoft), incluindo a própria documentação Intel Há várias diferenças entre os dois formatos Fazendo gcc -O1 -S -masm=intel ex11.c forçamos o uso do formato Intel (ver exemplo de código gerado)
Sufixos Sumário Devido a sua origem com 16 bits, a Intel usa o termo word para referenciar tipos de dados de 16 bits: b (byte), tamanho de um byte movb $0, (%eax) w (word), tamanho de dois bytes movw $0, (%eax) l (long), tamanho de quatro bytes movl $0, (%eax)
Nomenclatura para designar os registradores
Registradores de propósito geral do 80386
Registradores de propósito geral (GPRs) do 80386 Tinham finalidade específica nas arquiteturas anteriores de 16 bits (razão dos nomes), mas, com o endereçamento linear (flat addressing) de 4GB, o uso específico é enormemente reduzido e os seis primeiros podem ser considerados de uso geral, ainda que algumas instruções usem registradores fixos como fonte e destino EAX - Acumulador, usado em operações aritméticas. ECX - Contador, usado em loops. EDX - Registrador de dados, usado em operações de entrada/saída e em multiplicações e divisões. É também uma extensão do Acumulador. EBX - Base, usado para apontar para dados no segmento DS (8086). ESI - Índice da fonte de dados a copiar (Source Index). Aponta para dados a serem copiados para DS:EDI. EDI - Índice do destino de dados a copiar (Destination Index). Aponta para o destino dos dados a serem copiados de DS:ESI. ESP - Apontador da Pilha (Stack Pointer). Aponta para o topo da pilha (endereço mais baixo dos elementos da pilha). EBP - Apontador da base do frame (registro de ativação). Acesso a argumentos de procedimentos passados pela pilha.
Registradores de propósito específico do 80386
Registradores de segmento CS - Segmento do Código DS - Segmento de Dados ES - Segmento com dados extra FS - Segmento com mais dados GS - Segmento com ainda mais dados SS - Segmento da Pilha (Stack) Segmentos não existem em praticamente mais nenhuma arquitetura, exceto x86, pois segmentos existiam para contornar a limitação do espaço virtual Os compiladores geralmente optam por ignorar os segmentos e os SOs modernos para x86 (incluindo o Windows e o Linux) tipicamente fazem todos os registradores de segmento apontar para o mesmo segmento de 4GB, exceção aos registradores FS e o GS, que são usados para isolar as seções de dados das diferentes threads de um mesmo processo As outras arquiteturas utilizam registradores especiais chamados registradores de thread para esse efeito, os quais não existem no x86
Registrador das flags (EFLAGS) Armazena códigos de condições setadas por operacões lógicas e aritméticas. As condições das flags podem ser testadas por instruções específicas Utilizar um método convencional para acessar este registrador produz um erro do montador (assembler), pois o x86 não fornece nenhuma forma de acesso direto ao registrador das flags Para modificar ou ler o eflags Necessário utilizar a instrução pushf (16 bits) ou pushaf (32 bits)
Referências bibliográficas Computer Systems - A Programmer s Perspective (Cap.3) pt.wikipedia.org/wiki/x86