• Nenhum resultado encontrado

Porting do compilador LLVM com o frontend Clang para uma nova arquitetura de processador

N/A
N/A
Protected

Academic year: 2020

Share "Porting do compilador LLVM com o frontend Clang para uma nova arquitetura de processador"

Copied!
96
0
0

Texto

(1)

José Miguel Pereira da Silva

Porting do compilador LLVM com o frontend

Clang para uma nova arquitetura de

processador

José Miguel P er eir a da Silv a P or ting do com pilador LL

VM com o frontend Clang para uma no

va ar q uite tura de processador

Universidade do Minho

Escola de Engenharia

(2)
(3)

Tese de Mestrado

Ciclo de Estudos Integrados Conducentes ao Grau de

Mestre em Engenharia Eletrónica Industrial e Computadores

Trabalho efetuado sob a orientação do

Professor Doutor João Monteiro

José Miguel Pereira da Silva

Porting do compilador LLVM com o frontend

Clang para uma nova arquitetura de

processador

Universidade do Minho

Escola de Engenharia

(4)
(5)

Agradecimentos

Gostaria de agradecer ao meu orientador Professor Doutor João Monteiro. Gostaria também de agradecer aos docentes do Embedded Systems Research

Group do Departamento de Eletrónica Industrial da Universidade do Minho,

nomeadamente aos professores Adriano Tavares e Paulo Cardoso que me acompanharam no desenvolvimento desta tese.

Agradece os meus colegas de curso e amigos pela sua amizade e apoio ao longo de todo o meu percurso académico.

Agradeço á minha família sobretudo aos meus pais por me terem apoiado desde sempre e por todo esforço despendido para que eu pudesse concluir o meu percurso académico com sucesso.

(6)
(7)

Resumo

Os sistemas embebidos estão presentes em praticamente todos os aspetos do dia-a-dia da vida moderna. Os sistemas embebidos são usados em leitores mp3, telemóveis, consolas, câmaras digitais, leitores DVD. Novos microprocessadores são por vezes desenvolvidos para satisfazerem as necessidades de uma certa aplicação ou grupo de aplicaçõs sendo necessário acompanhar o desenvolvimento de novas arquiteturas de processadores com o desenvolvimento de novos compiladores para elas.

O M2up é um novo processador multi-threading desenvolvido in-house pelo que ainda não possui nenhum compilador. O principal objetivo desta tese é desenvolver um compilador para funcionar em conjunto com o assemblador já desenvolvido para o M2up. A infraestrutura LLVM foi utilizada como base para a criação de um backend LLVM para o processador M2up usando o Clang como frontend. O LLVM (Low Level

Virtual Machine) é uma estrutura de compilação desenhada para otimizar a compilação

de programas, através do fornecimento de informações de alto nível às transformações do compilador otimizando assim os tempos de compilação, ligação e execução. O LLVM tem vindo a ganhar popularidade entre os programadores como uma alternativa ao GCC (GNU Compiler Collection), porque o GCC tem um código fonte grande e antigo que pode ser lento e que pode gerar alguns erros.

(8)
(9)

Abstract

Embedded systems have an influence on virtually all aspects of day-to-day modern life. Mp3 players, mobile phones, videogame consoles, digital ameras, DVD players use embedded systems. New microprocessors architectures are developed to fulfill the needs of a certain application or group of applications, this development of new architectures needs to be accompanied with the development of new compilers to them.

M2up is a new multi-threading processor developed in-house and it doesn‟t have any compiler. The main focus of this thesis is developing a compiler to work with the already developed M2up assembler. The LLVM infrastructure was used as a base to develop a backend for de M2up architecture using clang as the frontend. LLVM (Low Level Virtual Machine) is a compiler infrastructure designed to optimize the compilation of programs by providing high-level information to compiler transformations optimizing the compile, linking and executing time. LLVM has been gaining popularity among developers as an alternative to GCC, because GCC as a large and old code-base that can be buggy and slow.

(10)
(11)

Índice

1 INTRODUÇÃO ... 1 1.1 Estado da Arte ... 2 1.2 Motivações e objetivos ... 3 1.3 Organização do documento... 4 2 INFRAESTRUTURA DO LLVM... 5 2.1 Design do LLVM ... 6 2.1.1 Frontends LLVM ... 7

2.1.2 Representação intermédia (IR) ... 8

2.1.3 Backends ... 9

2.2 Framework de geração de código ... 9

2.2.1 Descrição da máquina alvo ... 11

2.2.2 Seleção de Instruções... 16

2.2.3 Alocação de Registos... 17

2.2.4 Emissão de código ... 19

2.3 Resumo ... 19

3 M2UP MULTI-THREADING MICROPROCESSOR ... 21

3.1 M2up Pipelined Datapath Design ... 21

3.2 Conjunto de Registos ... 22

3.3 Conjunto de Instruções ... 22

3.3.1 Operações aritméticas e lógicas ... 23

3.3.2 Instruções de acesso à memória... 24

3.3.3 Instruções de controlo ... 25

3.3.4 Instruções Miscelâneas ... 26

3.4 Resumo ... 27

4 IMPLEMENTAÇÃO DO BACKEND LLVM M2UP ... 29

4.1 Informação geral do backend ... 31

4.2 Seleção de instruções ... 32

4.2.1 Construção do DAG inicial ... 32

4.2.2 Optimização do DAG ... 34

4.2.3 Legalização do DAG ... 35

4.2.4 Seleção de instruções no DAG ... 40

4.2.5 Informações das instruções não estáticas ... 45

4.3 Escalonamento de instruções ... 46

(12)

4.4.1 Ficheiro de descrição do conjunto de registos ... 48

4.4.2 Informações dos registos não estáticas ... 50

4.5 Inserção do prólogo e do epílogo ... 52

4.6 Passos personalizados ... 53

4.7 Emissão de código ... 53

4.8 Integração do novo backend no LLVM ... 54

4.9 Resumo ... 55

5 TESTES E RESULTADOS... 57

5.1 Backend M2up ... 57

5.2 Testes e Resultados Obtidos na Simulação ... 57

5.3 Resumo ... 64

6 CONCLUSÃO ... 65

6.1 Trabalho futuro ... 65

7 BIBLIOGRAFIA ... 67

(13)

Lista de Abreviaturas

ABI- Application Binary Interface ACK - Amsterdam Compiler Kit

CISC- Complex Instruction Set Computer DAG- directed acyclic graph

FSF - Free Software Foundation GCC -Gnu Compiler Collection IR- Intermediate Representation ISA - Instruction Set Architecture

LCC- Local C Compiler / Little C Compiler LLVM - Low Level Virtual Machine

MVT- Machine Value Type PC- Program Counter PSW- Program Status Word RAM- Random Access Memory

RISC- Reduced Instruction Set Computer ROM- Read-Only Memory

SIMD- Single Instruction, Multiple Data SSA- Static Single Assignment

(14)
(15)

Índice de Figuras

Figura 2.1- Compilador em 3 fases [17] ... 6

Figura 2.2- Retargability do compilador em 3 fases [17]... 6

Figura 2.3- Duas funções para adicionar dois números em código C fonte [20] ... 8

Figura 2.4-Código LLVM IR gerado a partir das funções em linguagem C da figura 2.3 fonte [20] ... 8

Figura 2.5- Sequência de passos de geração de código. ... 11

Figura 2.6- Classe Instruction definida pelo LLVM ... 14

Figura 2.7-Classe base Register fornecida pela framework de geração de código do LLVM ... 14

Figura 2.8- Classe InstrStage fornecida pela framework de geração de código do LLVM ... 15

Figura 2.9-Classe InstrItinData fornecida pela framework de geração de código do LLVM ... 16

Figura 3.1-Diagrama de blocos representando os principais componentes do M2up .... 21

Figura 3.2- Datapath do microcontrolador M2up ... 22

Figura 3.3-Formato base das instruções M2up ... 23

Figura 3.4- Formato das instruções aritméticas e lógicas que utilizam ... 23

Figura 3.5-Formato das operações aritméticas e lógicas com imediatos ... 24

Figura 3.6-Formato das operações aritméticas e lógicas de deslocamento de bits ... 24

Figura 3.7-Formato das instruções de LOAD e STORE ... 24

Figura 3.8-Formato das instruções LOAD e STORE que utilizam o PC para calcular o endereço de memória ... 25

Figura 3.9- Formato da instrução de salto incondicional que utiliza registo para calcular o salto (em cima) e da instrução de salto incondicional que utiliza um imediato com o PC para calcular o salto. ... 25

Figura 3.10- Formato dos saltos condicionais ... 26

Figura 3.11- Formato da instrução HALT e SYSENTER ... 26

Figura 3.12- Formato da Instrução MPSW ... 26

Figura 4.1- Diagrama de classes simplificada do backend LLVM do M2up. A vermelho as classes base da Framewrok de geração de código, a azual as classes geradas pelo Tablegen a partir dos ficheiros de descrição e a amarelo classes específicas do backend M2up implementadas. ... 30

Figura 4.2- Implementação da classe M2upTargetMachine ... 31

Figura 4.3-Construtor da classe M2upTargetMachine ... 32

Figura 4.4- Código LLVM IR para uma função recursiva que soma n números a partir do número n ... 33

Figura 4.5- DAG inicial para o bloco entry da função recursiva ... 34

Figura 4.6-SelectionDag após a primeira fase de otimização ... 35

Figura 4.7-Descrição das convenções de chamada definidas para o M2up ... 36

Figura 4.8-DAG para o bloco entry da função recurssiva depois do processo de legalização do Dag... 40

Figura 4.9- Definição da Classe M2upInst a partir da classe genérica Instruction fornecida pela framework ... 41

Figura 4.10- Classe ArithLogic implementada a partir da classe M2upInst. Contêm a informação sobre o formato das instruções aritméticas e lógicas ... 41

Figura 4.11-Formato da instrução ANDI ... 42

(16)

Figura 4.13-Definição da multiclasse ArithandLogic ... 43

Figura 4.14- Método SelectCode gerado automaticamente a partir dos ficheiros de descrição ... 44

Figura 4.15- Seleção de Instruções para o bloco entry do código LLVM IR da figura 4.5 ... 44

Figura 4.16- Método SelectCode no caso do nodo ser o M2upISD::CMPPSW ... 45

Figura 4.17- SelectionDag após a fase de seleção de isntruções. ... 45

Figura 4.18- Unidades funcionais e classes de itinerários de instruções do M2up definidos no ficheiro M2upSchedule.td... 47

Figura 4.19- Itinerário para as instruções aritméticas e lógicas do M2up. ... 48

Figura 4.20- Código para o bloco entry da Figura 4.4 após o passo de escalonamento de instruções. ... 48

Figura 4.21- Classes de Registos definidas a partir da classe Register fornecida pela Framework ... 49

Figura 4.22-Definição dos registos físicos do LLVM no ficheiro de descrição de registos ... 49

Figura 4.23-Definição da RegisterClass Reg_bank que contêm os Registos R0 até R7 49 Figura 4.24- Código para o bloco entry após o passo de alocação de registos ... 51

Figura 4.25- Código para o bloco entry após a emissão do prólogo e epílogo ... 53

Figura 4.26- Métodos printCCOperand() e printMemOperand() ... 54

Figura 4.27- Registo do M2up como Target do LLVM ... 55

Figura 5.1- Código c da função soma ... 57

Figura 5.2- Código LLVM IR gerado a partir do código c da figura 5.1 ... 58

Figura 5.3-Código Assembly gerado pelo backend M2up do LLVM ... 58

Figura 5.4-Resultado obtido na simulação do primeiro caso de teste. A branco os sinais de clock e reset, a azul o opcode das instruções, a verde os registos e a amarelo a memória. ... 59

Figura 5.5- Código c da função if_else... 59

Figura 5.6- Código LLVM IR gerado a partir do código C da Figura 5.5 ... 60

Figura 5.7- Código assembly M2up para a função if-else ... 60

Figura 5.8-Resultado obtido na simulação do segundo caso de teste. A branco os sinais de clock e reset, a azul o opcode das instruções, a verde os registos e a amarelo a memória e a lilás as flags do registo PSW... 61

Figura 5.9-Código c para do terceiro teste ... 62

Figura 5.10-Código LLVM IR gerado a partir do código C da Figura 5.9 ... 62

Figura 5.11-Código assembly M2up gerado para a função main a partir do código LLVM IR da Figura 5.10 ... 63

Figura 5.12-Código assembly M2up gerado para a função soma a partir do código LLVM IR da Figura 5.10 ... 63

Figura 5.13-Resultado obtido na simulação do terceiro caso de teste. A branco os sinais de clock e reset, a azul o opcode das instruções, a verde os registos e a amarelo a memória. ... 64

(17)

Índice de Tabelas

(18)
(19)

1 Introdução

Os sistemas computacionais sofreram uma grande evolução desde o início do século XX. Durante esta evolução surgiram os sistemas computacionais que são designados como sistemas embebidos. Um sistema embebido é um sistema que é desenvolvido com o objetivo de desempenhar um grupo limitado de tarefas por oposição a um computador de propósito geral que é desenhado para ser mais flexível e ser capaz de executar um maior número de tarefas diferentes. Os sistemas embebidos apresentam restrições severas quer ao nível de tamanho, eficiência ou consumo para que possam ser comercialmente viáveis. No entanto, os sistemas embebidos hoje em dia representam cerca de 98% de todos os chips eletrónicos produzidos atualmente [1] e estão presentes em muitos aspetos do nosso quotidiano tais como os telemóveis, mp3, câmaras digitais, leitores de DVD e leitores de GPS são utilizados diariamente.

O processador é o componente central de um sistema computacional e necessita de chegar a um compromisso entre as métricas de eficiência, tamanho e consumo para que possa cumprir os requisitos de um determinado sistema. Os processadores para sistemas de propósito gerais são geralmente processadores CISC (Complex Instruction

Set Computer) capazes de executar centenas de instruções complexas diferentes. Os

processadores para sistemas embebidos utilizam geralmente arquiteturas do tipo RISC (Reduced Instruction Set Computer), sendo outras arquiteturas tais como o VLIW (Very

Long Instruction Word) utilizadas para um nicho mais reduzido de aplicações. As

arquiteturas RISC apresentam um conjunto de instruções simples e pequeno e que possuem aproximadamente o mesmo tempo de execução. Existem variadas famílias de processadores baseados na arquitetura RISC como são os casos do MIPS, SPARC, ARM. Um novo processador RISC, o M2up foi desenvolvido na Universidade do Minho.

Um aspeto fundamental quando se desenvolve um novo processador é também desenvolver um conjunto de ferramentas de apoio ao mesmo sendo uma das mais importantes o compilador. As características de um bom compilador dependem obviamente do seu propósito sendo que gerar código correto é fundamental para todos eles. Tradicionalmente algumas das métricas que os compiladores seguiam eram por exemplo a quantidade de memória utilizada, a velocidade de compilação ou a qualidade das mensagens de erro produzidas [2]. Hoje em dia, para além destes já mencionados,

(20)

outros fatores tem vindo a ganhar importância. Um compilador deve ser modular, sustentável e deve ser facilmente extensível. Neste contexto surge o LLVM ( Low Level

Virtual Machine) que ao contrário dos compiladores tradicionais oferece uma estrutura

de compilação mais genérica e versátil. O LLVM é por isso particularmente interessante quando pretendemos desenvolver um compilador para uma nova arquitetura como a do M2up desenvolvido na Universidade do Minho.

1.1 Estado da Arte

O microcontrolador M2up é uma nova arquitetura de processador desenvolvido pela Universidade do Minho e por isso não possui ainda nenhum compilador nativo e também nenhum backend para o LLVM ou para qualquer outro compilador. Existe um grande número de compiladores para as linguagens C e C++, no entanto a maioria destina-se a apenas um sistema operativo e a uma arquitetura alvo. No entanto, existem atualmente dois grandes projetos que permitem o desenvolvimento de backends para novas arquiteturas e que são por isso ideias para quando se pretende desenvolver

software para várias arquiteturas diferentes ou para arquiteturas menos utilizadas. Esses

dois projetos são o GCC (Gnu Compiler Colection) e o LLVM.

Existem no entanto outros compiladores e conjuntos de ferramentas que permitem o desenvolvimento de compiladores C/C++ para novas arquiteturas. Um dos compiladores C que foi desenvolvido como um compilador retargetable é o LCC (Local C Compiler / Little C Compiler) [2]. O código fonte do LCC está disponível gratuitamente mas não é considerado open source porque produtos desenvolvidos a partir do mesmo não podem ser vendidos [3]. O LCC foi desenvolvido por Christopher Fraser e David Hanson. O LCC começou por ser um compilador para um subconjunto da linguajem C que tinha como objetivo ser usado no ensino sobre implementação de compiladores. O LCC evoluiu depois para um compilador para a linguagem ANSI C mas manteve o seu intuito de se manter um compilador simples, rápido e com um conjunto de otimizações reduzidas em comparação com outros compiladores mais sofisticados. O LCC procura assim a simplicidade e também a facilidade de efetuar o

porting para novas máquinas [2]. O LCC possuí backends para diversas arquiteturas tais

(21)

O ACK (Amsterdam Compiler Kit) é uma plataforma de cross-compiling e um conjunto de ferramentas desenvolvido por Andrew Tanenbaum e Ceriel Jacobs [4] [5]. Desenvolvido no início dos anos 80 foi um dos primeiros compiladores desenhado para suportar diversas linguagens e diversas plataformas. O ACK foi desenhado com o objetivo de ser pequeno, portável, rápido e flexível. O ACK contêm frontends para as linguagens C, ANSI C, K&R C, Pascal, Modula-2 e Occam 1. Suporta também várias arquiteturas: 6502, 6800 (assembler only), 6805 (assembler only), 6809 (assembler only), ARM, 8080, Z80, Z8000, i86, i386, SPARC, VAX4, PDP11.

O ACK começou por ser um produto comercial mas em 2003 passou a ser distribuído sobre uma licença open source BSD.

VBCC é um compilador C retargetable desenvolvido pelo Dr. Volker Barthelmann [6]. O VBCC fornece um grande conjunto de otimizações de alto nível e também ao nível da máquina alvo. O VBCC possuí backends para as seguintes arquiteturas: Coldfire, PowerPC, 80x86, Alpha, C16x/ST10, 68hc12, z-machine, 680x0. O Lance é um software que permite a implementação de compiladores para a linguagem C [7]. O Lance fornece um frontend para linguagem C baseado no sistema OX. Fornecendo a gramática pretendida o OX gera automaticamente os ficheiros Lex e Yacc. O Frontend do Lance gera um ficheiro com a representação intermédia no formato de 3-endereços. O Lance fornece também um conjunto de otimizações que estão implementadas como ferramentas separadas e que podem ser combinadas e estendidas de maneira a melhorar a qualidade do código para a máquina alvo específica. O código IR (Intermediate Representation) otimizado é depois convertido numa estrutura de dados compatível com geradores de código como o Iburg e o Olive.

1.2 Motivações e objetivos

O objetivo desta tese é o desenvolvimento de um compilador para as linguagens C e C++ para o processador M2up desenvolvido na Universidade do Minho. Esta tese visa assim dotar o M2up de uma nova ferramenta para além do assemblador já existente.

As linguagens C e C++ são duas das linguagens de programação mais utilizadas no mundo inteiro [8] [9]. Estas duas linguagens possuem características que as tornam uma boa escolha para projetos de sistemas embebidos e por isso praticamente todos os processadores possuem compiladores de C e C++. Um compilador para estas linguagens

(22)

que tenha como alvo o processador M2up permitirá assim desenvolver novas aplicações utilizando este processador como plataforma de hardware.

O desenvolvimento de compiladores é um dos ramos da computação mais bem-sucedidos sendo que as suas aplicações vão para além da construção de compiladores, pois os processos e algoritmos aplicados na construção de compiladores podem ser utilizados noutras áreas como por exemplo a conversão de ficheiros.

1.3 Organização do documento

Esta tese está dividida em 6 capítulos. O capítulo 1 é a Introdução e contêm o estado da arte assim como os objetivos e motivações para a realização da mesma.

O capítulo 2 contém uma descrição técnica do design do LLVM e da sua Framework para geração de código. Neste capítulo descreve-se os diferentes componentes da estrutura de compilação LLVM dando maior enfase á fase de geração de código.

O capítulo 3 descreve o ISA (Instruction Set Architecture) do microprocessador M2up e também o seu datapath. Neste capítulo encontra-se uma descrição simplificada do conjunto de instruções e registos do M2up.

No capítulo 4 encontram-se o design e a implementação do backend LLVM para o micro M2up. Os diferentes componentes do backend e as interações entre os mesmos estão descritos neste capítulo.

O capítulo 5 é o capítulo de testes e resultados. Neste capítulo estão descritos os testes e respetivos resultados utilizados para validar a implementação.

A conclusão é o capítulo 6. Na conclusão analisa-se todo o trabalho efetuado assim como trabalho futuro que pode ser realizado na área desta tese.

(23)

2 Infraestrutura do LLVM

O LLVM é uma estrutura de compilação desenhada para otimizar a compilação de programas, através do fornecimento de informações de alto nível às transformações do compilador otimizando assim os tempos de compilação, ligação e execução. Começou a ser desenvolvido em 2000 por Chris Lattner na University of Illinois [10]. O código do LLVM foi desenvolvido utilizando a linguagem C++ e é um software

opensource [11].

O LLVM tem vindo a ganhar notoriedade e é já utilizado em diversos projetos quer académicos quer comerciais. Um dos projetos que utiliza o LLVM é o projeto MacRuby [12] da Apple. Este projeto utiliza algumas passos de otimização do LLVM e também alguns passos do compilador LLVM JIT.

O OpenJDK é um projeto da Sun Microsystems para a criação de um JDK (Java

Development Kit) mas nem todos os componentes usados para o construir são de

software livre [13]. Neste contexto surgiu o projeto IceTea [14] que foi desenvolvido para construir OpenJDK usando apenas software livre. Uma das extensões adicionada ao projeto IceTea foi um compilador JIT chamado Shark [15] que utiliza o LLVM para . expandir as plataformas que suportam OpenJDK, que eram apenas os processadores x86 e Sparc, para todas as plataformas que tenham um backend JIT do LLVM. A Ascenium Corporation utiliza os bytecodes do LLVM como input para o seu gerador de código para o processador Ascenium.

O LLVM tem vindo a ganhar popularidade entre os programadores como uma alternativa ao GCC. O GCC é um compilador para um conjunto de linguagens de programação tais como C, C++, Objective-C, Fortran, Java, Ada e Go. O GCC desenvolvido

pelo projeto GNU tinha como objetivo inicial ser o compilador do sistema operativo GNU. O

projeto é distribuído pela FSF (Free Software Foundation) sob os termos da GNU GPL e é portanto um software absolutamente livre [16].

O LLVM difere dos sistemas tradicionais como o GCC pois apresenta uma abordagem radicalmente diferente em termos de design. Ao contrário do GCC, o LLVM não é apenas um compilador mas sim uma estrutura de compilação modular. Esta característica do LLVM está bem presente na sua arquitetura que é baseada em bibliotecas e em que cada componente está separado de forma bem definida. Isto permite ao LLVM ter uma grande facilidade de expansão e permite também que outras

(24)

aplicações utilizem alguns dos seus componentes. Esta facilidade de expansão levou à decisão de desenvolver o compilador para o M2up utilizando a infraestrutura do LLVM.

2.1 Design do LLVM

O design mais popular utilizado num compilador estático tradicional é um desenho em três fases. Os principais componentes deste design são o frontend, o optimizador e o backend (Figura 2.1).

Figura 2.1- Compilador em 3 fases [17]

O frontend tem como função analisar o código fonte fazendo uma análise léxica e sintática para detetar erros no código e construir uma árvore de sintaxe (AST). Esta árvore de sintaxe pode ser convertida noutro tipo de código intermédio como por exemplo um código de 3-endereços. O optimizador utiliza uma série de transformações com o objetivo de melhorar o tempo de execução do código por exemplo otimizando ciclos aninhados ou removendo instruções redundantes. O backend gera código correto para o ISA da máquina alvo definida. Os principais componentes de um backend são geralmente a seleção e escalonamento de instruções e a atribuição de registos. A grande vantagem desta divisão em três fases, se existir um optimizador comum, é permitir gerar código para diversas linguagens diferentes ou diferentes targets alterando apenas um os componentes (Figura 2.2).

(25)

LLVM utiliza um design em 3 fases. O frontend é responsável por transformar o código numa representação intermédia (LLVM IR). Este código LLVM IR é sujeito a uma série de passos de passos de otimização e no backend será transformado em código máquina para o alvo definido.

2.1.1 Frontends LLVM

O LLVM não incluiu nenhum frontend para uma linguagem específica. Existem no entanto dois frontends desenvolvidos pela equipa do LLVM, o LLVM-GCC e o Clang.

2.1.1.1 LLVM-GCC

Este frontend baseia-se no conjunto de frontends já existentes do GCC nomeadamente no GCC 4.2 da Apple. O LLVM-GCC transforma então a representação GIMPLE do GCC em linguagem LLVM. Esta abordagem permitiu ao LLVM suportar o conjunto de linguagens de programação do GCC. No entanto o frontend GCC é bastante lento e consume bastante memória.

2.1.1.2 Clang

O clang é um frontend desenvolvido de raiz para o projeto LLVM. O clang suporta atualmente as linguagens C, C++ e Objective C. O clang apresenta um conjunto de vantagens em relação ao GCC [17] [18]:

 As AST do clang foram desenvolvidas de forma a serem facilmente entendidas para quem tenha conhecimentos sobre o funcionamento de um compilador enquanto que o gcc apresenta um código base bastante antigo que torna mais difícil a aprendizagem a quem queira desenvolver novas soluções.

 O clang está desenhado como uma API ao contrário do GCC que é um compilador monolítico e estático sendo por isso mais difícil incluí-lo noutras ferramentas.

O clang fornece um diagnóstico (warnings e erros) mais claro e conciso.

(26)

As desvantagens do clang são suportar um menor número de linguagens e targets e ser menos utilizado que o gcc existindo assim um menor suporte por parte da comunidade open-source.

2.1.2 Representação intermédia (IR)

O aspeto mais importante do desenho do LLVM é a sua representação intermédia LLVM IR. O seu aspeto mais importante é ser definida como uma linguagem própria com semântica bem definida. Considerando o código C da Figura 2.3 o código LLVM IR correspondente seria o da Figura 2.4.

Figura 2.3- Duas funções para adicionar dois números em código C fonte [20]

(27)

Como se pode observar na Figura 2.4, o LLVM IR é uma espécie de representação de um instruction set de um processador RISC mas que contem apenas instruções simples como add, subtract, compare ou branch. Estas instruções estão representadas na forma SSA (Static Single Assignment). O LLVM IR suporta também

labels. A descrição completa da linguagem LLVM está descrita no LLVM Language Manual Reference [20].

O desenvolvimento de um frontend está então apenas dependente do conhecimento de como funciona o LLVM IR. Visto que o LLVM IR é em si uma forma de linguagem textual é possível desenhar um frontend que tem como output uma representação LLVM IR em texto. Esta propriedade simplifica o desenvolvimento de novos frontends para o LLVM.

Esta representação intermédia é o único interface entre o optimizador e o

frontend e backend não ficando assim dependente de uma linguagem ou target

específico, no entanto deve servir ambos bem, permitindo assim gerar código de melhor qualidade.

2.1.3 Backends

O backend é o responsável pela geração de código. A função do gerador de código é a de transformar o código LLVM IR em código máquina para um determinado alvo. O LLVM utiliza um gerador de código geral pois a maior parte das máquinas alvo necessita de efetuar processos semelhante tais como atribuir valor aos registos ou selecionar instruções, embora cada arquitetura tenha instruções e registos diferentes alguns algoritmos podem ser partilhados. Nas próximas secções será descrita a Framework de geração de código fornecida pelo LLVM assim como as diferentes etapas essenciais a um gerador de código tais como seleção de instruções, alocação de registos e emissão de código.

2.2 Framework de geração de código

O LLVM code generator [21] é uma framework que fornece um conjunto de componentes reutilizáveis para permitir a tradução do LLVM IR em código máquina, quer seja em formato assembly ou binário. Consiste essencialmente de 4 componentes:

 Um conjunto de interfaces abstratas que permitem especificar as características de uma determinada máquina alvo;

(28)

 Algoritmos usados para implementar várias fases da geração de código (seleção de instruções, alocação de registos, escalonamento de instruções, representação da stack),

Diversas implementações de backends pertencentes ao projeto LLVM.

A geração de código utilizando a Framework será feita função a função, ou seja, cada uma das funções passa por todos os processos de geração de código individualmente. O processo de geração de código, utilizando esta Framework está dividido em sete passos distintos:

Seleção de Instruções: nesta fase, o código intermédio LLVM é transformado de modo a usar a utilizar as instruções da máquina alvo. O código intermédio é nesta fase transformado num DAG (directed acyclic graph) de instruções da máquina alvo como pode ser observado na Figura 2.5.

Escalonamento de Instruções: o grafo de instruções gerado na fase de anterior é ordenado conforme as restrições da máquina alvo.

Otimizações na forma SSA: esta é uma fase opcional que consiste numa série de otimizações ao código na forma SSA tais como modulo-scheduling ou

peephole.

Alocação de registos: neste passo são eliminadas as referências aos registos virtuais sendo atribuídos os registos físicos concretos da máquina alvo.

Inserção do prólogo e do epílogo: como o código máquina para a função já foi gerado e o tamanho da stack necessário já é conhecido, o prólogo e o epílogo já podem ser inseridos na função.

Otimizações finais: é o último passo de otimização serve para eliminar as redundâncias que não foram encontradas nos passos de otimização anteriores.

Emissão de código: é o passo final, emitindo o código no formato assembly da máquina alvo ou em formato binário.

(29)

Selecção de Instruções Escalonamento Optimizações na forma SSA Alocação de Registos Inserção do prólogo e epílogo Otimizações finais LLMV IR Emissão de código Ficheiro Assembly

Figura 2.5- Sequência de passos de geração de código.

Nas próximas secções serão abordados em maior detalhe os passos mais importantes da geração de código utilizados para o desenvolvimento de um backend para uma nova arquitetura.

2.2.1 Descrição da máquina alvo

Os processos apresentados na Figura 2.5 necessitam de um conjunto de informações relativos á máquina alvo para que todo o processo de geração de código funcione corretamente. A descrição das características de uma máquina alvo é feita recorrendo a uma série de classes abstratas em C++, que estão definidas na Framework de geração de código. As principais classes para descrição de uma máquina alvo são:

TargetMachine: fornece métodos virtuais que permitem o acesso às implementações das diferentes classes que descrevem um determinado target.

TargetData: é a única classe de descrição absolutamente necessária, nela se definem informações sobre o target tais como organização da memória, definições de alinhamentos para os diferentes tipos de dados, tamanho do apontador, se o target é litle-endian ou big-endian.

TargetLowering: esta classe é utilizada para descrever como é que o código LLVM deve ser transformado num grafo de operações. Nesta classe indica-se quais os registos que devem ser usados para cada tipo de

(30)

dados, as operações que são suportadas nativamente pela máquina alvo, algumas características de alto nível como por exemplo se é compensatório transformar a divisão por uma constante numa sequência de multiplicações.

TargetRegisterInfo: é utilizada para descrever o conjunto de registos do target e todas as interações entre registos.

TargetInstrInfo: é utilizada para descrever o conjunto de instruções suportadas pelo target.

TargetFrameInfo: é utilizada para descrever a organização da pilha do target.

Estas classes serão a base para a implementação das classes do backend que se pretende desenvolver. As classes implementadas a partir destas classes base anteriormente referidas devem fornecer informação detalhada sobre as características da máquina alvo, sendo que muita desta informação acaba por ser comum a diversas instancias, por exemplo uma instrução add é idêntica a uma instrução sub em diversos aspetos. Para evitar esta repetição de informação o gerador de código utiliza a ferramenta TableGen para descrever diversos aspetos da máquina alvo reduzindo a quantidade de repetição de código através da utilização de abstrações específicas a um determinado domínio ou alvo. A utilização do TableGen não é obrigatória na implementação de um backend mas devido á sua utilidade e facilidade de utilização todos os backends utilizam esta ferramenta para gerar parte do seu código. Um dos objetivos do LLVM é que no futuro seja possível gerar backends apenas a partir de ficheiros de descrição TableGen, no entanto isso ainda não é possível sendo por isso necessário implementar uma série de métodos para lidar com os componentes que ainda não podem ser gerados pelo TableGen.

2.2.1.1 Tablegen

O TableGen é uma ferramenta do LLVM utilizada pela framework de geração de código. Esta ferramenta é utilizada para gerar código C++ a partir de ficheiros de descrição que utilizam uma sintaxe própria do TableGen. Estes ficheiros podem ser ficheiros de descrição de Registos, Instruções, Convenções de Chamada ou

(31)

Escalonamento de Instruções. O mesmo ficheiro de descrição pode ser usado para gerar código a ser incluído em mais de que uma das classes referidas anteriormente.

Os ficheiros de descrição no formato TableGen consistem de duas partes fundamentais: classes e definições sendo que ambos são considerados records. As definições são a forma concreta de um record, são um identificador associado a uma lista de atributos e seus respetivos valores e são marcadas com a palavra-chave „def‟. As classes são a forma abstrata de um record e são utilizadas para construir outros records, agregando informações que se repetem entre as diversas definições. Existem também multiclasses que são grupos de classes que são instanciadas de uma só vez podendo cada uma delas instanciar múltiplas definições. Uma mutliclasse pode por exemplo ser definida para instanciar uma classe de operações aritméticas e lógicas que utiliza apenas registos como operandos e outra que utiliza registos e imediatos. A partir daí pode ser criada uma definição múltipla de uma instrução utilizando a palavra-chave „defm‟ definindo assim, por exemplo uma instrução ADD e uma instrução ADDI.

A Framework do LLVM fornece vários ficheiros de descrição TableGen a partir dos quais se podem implementar ficheiros de descrição relativos ao target que se pretende implementar. Um conjunto de ficheiros de descrição podem ser implementados para descrever o conjunto de instruções, registos, convenções de chamada e o escalonamento de instruções.

2.2.1.2 Ficheiro de descrição das Instruções

O ficheiro de descrição de instruções deverá fornecer ao gerador de código do LLVM informações sobre as instruções da máquina alvo tais como número de operandos, tipo de operação ou a string assembly a ser usada pelo emissor de código.

O LLVM fornece uma classe de instruções genéricas, a classe Instruction (Figura 2.6) presente no ficheiro Target.td (os ficheiros no formato TableGen têm como extensão .td) a partir da qual podem ser implementadas diversas classes de instruções para os diferentes tipos de instruções. Estas classes por sua vez serão usadas para definir cada uma das instruções da máquina alvo.

(32)

Figura 2.6- Classe Instruction definida pelo LLVM

O TableGen será usado para gerar código C++ a partir deste ficheiro de descrição que será utilizado no processo de seleção de instruções e de emissão de código.

2.2.1.3 Ficheiro de descrição dos registos

No ficheiro de descrição dos registos descreve-se o conjunto de registos da máquina alvo. Estas informações podem ser o tamanho dos registos, o tipo de dados que suportam ou quantos registos dos diferentes tipos a máquina alvo contém.

O LLVM fornece uma classe base Register (Figura 2.7) presente no ficheiro Target.td a partir da qual podem ser definidas diversas classes de registos para agrupar os registos da máquina alvo.

Figura 2.7-Classe base Register fornecida pela framework de geração de código do LLVM

O TableGen utilizará este ficheiro para gerar código que irá ser utilizado no passo de alocação de registos.

(33)

2.2.1.4 Ficheiro de descrição das convenções de chamada

As convenções de chamada também podem ser definidas num ficheiro de descrição TableGen. O ficheiro define as convenções de chamada através da utilização de um conjunto de condições e ações. A condição CCIfType define por exemplo quais os tipos de dados aos quais pode ser efetuado uma ação. As ações podem ser por exemplo a CCAssignToReg que atribui o dado a um registo ou CCAssignToStack que atribui o dado à stack. Todas as condições e ações que podem ser usadas para definir as convenções de chamada para uma determinada máquina alvo estão definidas no ficheiro TargetCallingConv.td da Framework de geração de código.

2.2.1.5 Ficheiro de descrição do escalonamento de instruções

O gerador de código LLVM é responsável pela tarefa de scheduling. O scheduler do LLVM necessita no entanto de receber um conjunto de informações sobre a máquina alvo para efetuar um escalonamento mais eficiente e correto.

A Framework de geração de código do LLVM fornece dois ficheiros de descrição, o TargetSchedule.td e o TargetItinerary.td a partir dos quais podem ser implementados ficheiros de descrição para descrever como deve ser efetuado o escalonamento das instruções na máquina alvo. Estes ficheiros disponibilizam a classe InstrStage a partir da qual pode ser definido o número de ciclos que uma instrução demora num determinado estágio (Figura 2.8).

Figura 2.8- Classe InstrStage fornecida pela framework de geração de código do LLVM

Além da classe InstrStage estes ficheiros fornecem também a classe InstrItinData que permite agrupar as diferentes InstrStage para uma determinada instrução ou grupo de instruções (Figura 2.9).

(34)

Figura 2.9-Classe InstrItinData fornecida pela framework de geração de código do LLVM

2.2.2 Seleção de Instruções

O problema de seleção de instruções é encontrar uma maneira eficiente de mapear o código intermédio gerado por um compilador que é independente do target num programa em linguagem máquina de uma máquina alvo específica. O tamanho de um código que é muitas vezes ignorado quando estamos a falar de máquinas de propósito geral, torna-se muito importante quando o target é um sistema embebido que tem geralmente um espaço de memória limitado para armazenar e executar código. Idealmente o seletor de instruções deveria ser capaz de encontrar uma solução ótima para transformar o código intermédio em código máquina.

A abordagem clássica para o processo de seleção de instruções é através da reescrita de árvores de expressão. Existem diversos algoritmos para otimizar um processo de rescrita de árvores. A melhor solução pode corresponder à menor sequência de instruções ou por exemplo ao menor tempo de execução, visto que diferentes instruções têm tempos de execução diferentes.

[22] Aho e Johnson foram os primeiros a propor uma abordagem dinâmica ao problema de seleção de instruções para procurar uma solução ótima. O algoritmo dinâmico atribuiu um custo a cada ramo da árvore, sendo o custo final o somatório do custo das diferentes instruções da melhor sequência capaz de traduzir esse ramo da árvore. O algoritmo funciona de forma bottom-up, ou seja, primeiro são calculados recursivamente os custos dos filhos, netos, etc. de cada nodo, sendo que só depois é que é feito o “matching”entre os patterns e o nodo.

Outro algoritmo utilizado para procurar soluções ótimas para o problema de seleção de instruções é o Maximal Munch desenvolvido por Cattel [23]. O Maximal

(35)

procura encontrar o maior pattern que encaixe, fazendo de seguida o mesmo para cada uma das subárvores. Este algoritmo gera as instruções na ordem inversa, a raiz da árvore só pode ser executada quando as outras instruções já tenham produzido valores nos registos. Ao contrário do algoritmo de programação dinâmica o Maximal Munch é bastante simples de implementar.

O LLVM não utiliza uma seleção baseada na reescrita de árvores mas sim uma seleção de instruções baseada num grafo acíclico direcionado (em inglês: directed acyclic graph, ou simplesmente um dag ou DAG), denominado de SelectionDAG. Um DAG é um grafo direcionado sem ciclo, ou seja, para qualquer vértice v do grafo não há uma ligação direcionada começando e acabando em v. Ao contrário do que foi visto para árvores, encontrar uma solução ótima para um DAG é um problema NP-Completo. O seletor de instruções utiliza os padrões de seleção disponibilizados pelo backend para guiar as suas decisões.

O processo de seleção de instruções disponibilizado pelo LLVM consiste dos seguintes passos:

1- Construção do Dag inicial: nesta fase processa-se uma simples transformação do código LLVM num grafo denominado “ilegal” pois utiliza instruções e tipos de dados não suportados pela máquina alvo.

2- Otimização do DAG: neste passo são realizadas algumas otimizações simples para simplificar o grafo.

3- Legalização do DAG: neste passo o grafo é transformado de modo a substituir as instruções e tipos de dados que não são suportados pela máquina alvo.

4- Otimizações: novo passo de otimização utilizado para eliminar as redundâncias que possam vir a ser introduzidas pelo processo de legalização do grafo.

5- Seleção de instruções no DAG: O algoritmo de seleção de instruções é executado, transformando o SelectionDag num Dag de instruções da máquina alvo.

6- Escalonamento e formação: Nesta última fase é delineada uma ordem linear para as instruções do grafo legalizado gerado no passo anterior.

2.2.3 Alocação de Registos

Instruções que envolvem apenas registos são mais rápidas do que as que utilizam operandos de memória. Hoje em dia, a velocidade dos processadores é bastante

(36)

elevada mas o constante acesso á memória pode diminuir a velocidade de um determinado processo. É por isso vital, que os registos sejam utilizados de forma eficiente para que seja possível gerar bom código.

Após o processo de seleção de instruções, o código já utiliza apenas instruções da máquina alvo mas continua a utilizar registos virtuais. O passo de alocação de registo consiste por isso em mapear um programa que utiliza um número infinito de registos virtuais, num programa que contêm um número finito de registos físicos.

No LLVM os registos são definidos como números inteiros que variam normalmente entre o 1 e o 1023. Para cada target podemos aceder ao ficheiro gerado a partir do seu ficheiro de descrição de registos para ver com que valores foram mapeados os diferentes registos. Algumas arquiteturas possuem diferentes registos mas que partilham uma parte da localização física, por exemplo no x86 os registos EAX,AX e Al partilham os primeiros 8 bits. Estes registos são marcados como aliased no LLVM.

Os registos físicos no LLVM são agrupados em classes de registos. Os elementos de cada classe são equivalentes e podem ser usados indistintamente entre eles. Cada registo virtual pode apenas ser mapeado para um registo de uma determinada classe, por exemplo, alguns registos virtuais poderão ter de ser mapeados num grupo de registos de 8 bits e outros num grupo de registos de 16 bits. Tal como os registos físicos, os registos virtuais também são denotados por números inteiros mas no entanto diferentes registos não podem partilhar o mesmo número.

Antes da fase de alocação de registos, a maior parte das instruções utilizam registos virtuais embora por vezes sejam utilizados registos físicos por exemplo na passagem de argumentos em function calls ou para guardar resultados de uma operação específica. A estes registos é atribuído a designação de pre-colored registers que vão impor algumas restrições á alocação de registos.

O LLVM dispõe de duas maneiras para mapear os registos virtuais em registos físicos ou endereções de memória. O mapeamento direto que utiliza métodos da classe TargetRegisterInfo e da classe MachineOperand (classe que contém métodos para manipular os operandos das instruções) e mapeamento indireto que utiliza a classe VirtRegMap que é utilizada para inserir loads ou stores enviando e recebendo assim valores da memória.

O mapeamento direto permite uma maior flexibilidade para quem desenvolve o alocador de registos mas é mais propício a erros. O programador terá de indicar

(37)

especificamente onde irão ser inseridas os loads e os stores utilizando os métodos

storeRegToStackSlot e loadRegFromStackSlot. Para atribuir um registo virtual a um

registo físico utiliza-se o método MachineOperand::setReg(p_reg).

No mapeamento indireto o developer não terá de se preocupar com a complexidade da inserção de loads e stores, sendo utilizado o método

VirtRegMap::assignVirt2StackSlot(vreg) que retorna o endereço da stack onde o registo virtual deverá ser guardado. Para mapear um registo virtual para um físico utiliza-se o método VirtRegMap::assignVirt2Phys(vreg, preg).

2.2.4 Emissão de código

Após a alocação de registos e de algumas otimizações finais será emitido o código final em linguagem máquina. O LLVM disponibiliza classes como a ASMprinter a partir da qual será implementada uma classe específica para cada target que em conjunto com as informações recolhidas a partir do ficheiro de descrição das instruções informará o gerador de código das características léxicas e sintáticas da linguagem de montagem da máquina alvo.

2.3 Resumo

O LLVM apresenta um design em 3 fases. O frontend responsável pela análise léxica e sintática do código fonte e geração do código intermédio, a fase de otimizações e o backend que tem como função transformar o código intermédio otimizado em código máquina para um determinado alvo. O LLVM fornece uma Framework de geração de código a partir da qual é possível implementar um backend para uma nova arquitetura. A Framework consiste numa série de classes genéricas que permitem descrever uma determinada máquina alvo e num conjunto de algoritmos responsáveis por algumas das etapas essenciais de um gerador de código tais como a alocação de registos, seleção de instruções ou escalonamento de instruções.

No próximo capítulo será feita uma descrição do ISA da máquina alvo: o microprocessador M2up.

(38)
(39)

3 M2up Multi-Threading

Microprocessor

O micro controlador M2up desenvolvido in-house é um micro de 16 bits

multithreading. Na Figura 3.1 estão representados os principais componentes do

microcontrolador M2up, o CPU (Central Processing Unit) e o conjunto de memórias. O M2up apresenta uma hierarquia de memória, tanto na memória de instruções como na memória de dados, utilizando para além das memórias principais ROM (Read-Only

Memory) e RAM (Random Access Memory), memórias caches direct mapped.

Memória de Instruções

(ROM) Cache de Instruções CPU Cache de Dados

Memória de Dados (RAM)

Figura 3.1-Diagrama de blocos representando os principais componentes do M2up

3.1 M2up Pipelined Datapath Design

O M2up apresenta um pipeline de cinco estágios, fetch, decode, execution,

memory access e write-back. Nem todas as instruções do M2up necessitam de passar

pelos cinco estágios do pipeline, por exemplo uma instrução aritmética e lógica não necessita de aceder á memória podendo por isso passar diretamente para o estágio de write-back. Na Figura 3.2 pode ser observado o datapath para o micro M2up com as principais unidades funcionais do M2up. Inicialmente é efetuado o fetch das instruções a partir da cache de instruções, no segundo estágio é efetuado o decoding das instruções num bloco denominado de frontend. O frontend é responsável pelo decoding das instruções mas também pela atualização do PC (Program Counter) e contêm o

Instruction Register, o Register File e uma unidade de controlo das interrupções. A

ALU é responsável pelo estágio de execution seguindo-se a fase de memory access em que as instruções acedem á cache de dados, por fim é efetuado o write-back que actualiza o Register File do Frontend. A Hazard Unit é responsável por verificar e lidar com potenciais hazards que possam existir.

(40)

Figura 3.2- Datapath do microcontrolador M2up

3.2 Conjunto de Registos

O banco de registos é constituído por 8 registos de 16 bits. Os registos R1 a R6 são de propósito geral. O registo R0 tem sempre o valor zero e o registo R7 guarda o valor do endereço de retorno após um salto (Tabela 3.1). O PSW (Program Status

Word) é um registo especial que guarda as flags Carry, Zero, Equal, Greater e Less que

vão ser utilizadas para determinar se um salto é tomado ou não.

Tabela 3.1-Conjunto de Registos

R0 Tem sempre valor 0. Escrever para este registo não tem efeito

R1 Propósito Geral R2 Propósito Geral R3 Propósito Geral R4 Propósito Geral R5 Propósito Geral R6 Propósito Geral

R7 Depois de uma instrução de salto o R7 guarda o valor de retorno

(valor do endereço da instrução mais um)

3.3 Conjunto de Instruções

O M2up apresenta um tamanho de instrução fixa de 16 bits. O formato dos vários tipos de instruções é semelhante garantindo assim um alinhamento que facilita a

(41)

descodificação das instruções. As instruções do M2up estão divididas em quatro grupos: as instruções aritméticas e lógicas, instruções de acesso à memória, instruções de controlo e instruções miscelâneas.

Figura 3.3-Formato base das instruções M2up

O opcode da instrução é de 5 bits e está definido nos bits mais significativos (Figura 3.3). No M2up o mesmo opcode pode definir diferentes instruções. Para se distinguir as instruções com o mesmo opcode são utilizados bits de seleção que serão definidos nas secções seguintes.

3.3.1 Operações aritméticas e lógicas

As operações ariméticas e lógicas no M2up podem usar apenas registos como operandos ou registos e imediatos. As operações aritméticas e lógicas que não são deslocamento de bits e só utilizam registos como operandos têm o formato apresentado na Figura 3.4.

Figura 3.4- Formato das instruções aritméticas e lógicas que utilizam

O bit i, que é o primeiro bit da instrução deve vir a zero indicando assim que esta se trata de uma instrução que não utiliza imediatos. As instruções que utilizam imediatos e registos apresentam um formato semelhante, sendo que um dos operandos deixa de ser um registo para ser um imediato (Figura 3.5).

(42)

Figura 3.5-Formato das operações aritméticas e lógicas com imediatos

Nestas instruções o bit i terá o valor um indicando assim que é uma instrução que utiliza imediatos. O bit i permite a distinção entre por exemplo uma instrução AND e uma instrução ADDI, pois apesar de ambas terem o mesmo opcode apenas no ADDI o bit menos significativo é um.

As instruções aritméticas e lógicas de deslocamento de bits têm um formato semelhante mas utilizam também o segundo bit menos significativo para indicar a utilização ou não do carry (Figura 3.6).

Figura 3.6-Formato das operações aritméticas e lógicas de deslocamento de bits

3.3.2 Instruções de acesso à memória

No M2up o acesso à memória pode ser efetuado apenas recorrendo a instruções LOAD ou STORE.

Figura 3.7-Formato das instruções de LOAD e STORE

O formato das instruções de LOAD e STORE pode ser observado na Figura 3.7. O bit menos significativo indica a utilização de imediato e o 2 bit menos significativo B indica se é uma operação ao byte ou à word de 16 bits. Existe ainda um outro tipo de LOAD/STORE que utiliza o PC para calcular o endereço de memória, o formato destas instruções é o apresentado na Figura 3.8.

(43)

Figura 3.8-Formato das instruções LOAD e STORE que utilizam o PC para calcular o endereço de memória

Nestas instruções o endereço é calculado somando o PC ao imediato de 6 bits que está entre o bit 2 e o 8.

3.3.3 Instruções de controlo

O M2up tem saltos incondicionais e saltos condicionais. Os saltos incondicionais podem utilizar registos e imediatos para calcular o novo PC ou então utilizar o PC com um offset.

Figura 3.9- Formato da instrução de salto incondicional que utiliza registo para calcular o salto (em cima) e da instrução de salto incondicional que utiliza um imediato com o PC para calcular o salto.

Como pode ser observarvado na figura 3.7 o salto pode ser calculado somando um registo a um imediato ou então somando o PC a um imediato.

Os saltos condicionais utilizam o PSW para verificar se um salto deve ser tomado ou não. Por exemplo, no caso de um BG (Branch-on-greater) se a flag Greater do registo PSW não estiver ativa o salto não deve ser tomado, caso contrário o salto deve acontecer. Ao contrário dos saltos incondicionais, os saltos condicionais são sempre referentes ao PC.

(44)

Figura 3.10- Formato dos saltos condicionais

O formato dos saltos condicionais pode ser observado na Figura 3.10. Visto que existem cinco saltos condicionais diferentes os últimos três bits são utilizados para distingui-los visto que todos têm o mesmo opcode.

3.3.4 Instruções Miscelâneas

As instruções miscelâneas são o HALT, o SYSENTER e o MPSW. O SYSENTER é a primeira instrução de cada código. O HALT é utilizado para impedir o fetching das instruções e o MSPW é uma instrução que permite alterar o PSW.

Figura 3.11- Formato da instrução HALT e SYSENTER

As instruções de HALT e SYSENTER utilizam apenas o opcode sendo os restantes bits indiferentes (figura 3.9).

Figura 3.12- Formato da Instrução MPSW

A instrução MPSW utiliza três bits para definir qual a flag que se pretende alterar. Se o bit „a‟ estiver a 0 faz-se o toogle da flag, se o bit a estiver a 1 a flag toma o valor definido no bit „b‟. Os restantes bits são indiferentes (figura 3.10).

O ISA completo do microcontrolador M2up pode ser encontrado em anexo no Apendice A.

(45)

3.4 Resumo

O M2up é um microcontrolador de 16 bits multi-threading e pipelined desenvolvido na Universidade do Minho. Apresenta um pipeline de cinco estágios embora nem todas as instruções tenham de passar pelos cinco estágios.

O M2up em um conjunto de registos reduzido sendo que apenas 6 são de propósito geral. Um dos registos do M2up é o PSW que contem as flags Carry, Zero, Equal, Greater e Less que serão utilizadas pelas instruções de salto condicional. Os valores destas flags são afetados pelas instruções aritméticas e lógicas.

O M2up tem quatro grupos de instruções diferentes: instruções aritméticas e lógicas, instruções de acesso à memória, instruções de controlo e instruções miscelâneas. O formato das instruções dos diferentes tipos de instrução é semelhante mantendo assim um alinhamento que facilita a descodificação das instruções.

No próximo capítulo descreve-se a implementação do backend LLVM para o processador M2up.

(46)
(47)

4 Implementação do Backend LLVM

M2up

O LLVM como foi visto no capítulo 2 fornece uma Framework de geração de código. Esta Framework foi utilizada para implementar o backend LLVM para o micro M2up. A Framework fornece um conjunto de classes base a partir das quais são implementadas subclasses com informações específicas relativas á máquina alvo. Na Figura 4.1 podemos observar uma versão simplificada do diagrama de classes do

backend. Para além destas classes base a Framework fornece um conjunto de algoritmos

para implementar as várias fases da geração de código tais como seleção de instruções, alocação de registos, escalonamento de instruções. Estes algoritmos são independentes do target e por isso podem ser usados para implementar geradores de código para diferentes arquiteturas de processadores. Estes algoritmos necessitam no entanto de informação específica da máquina alvo para que a geração de código para essa máquina funcione corretamente.

(48)
(49)

Framewrok de geração de código, a azual as classes geradas pelo Tablegen a partir dos ficheiros de descrição e a

amarelo classes específicas do backend M2up implementadas.

4.1 Informação geral do backend

A definição de um novo backend passa numa primeira fase pela implementação de uma classe a partir da qual será possível aceder aos diferentes componentes do backend. Esta classe poderia ser implementada diretamente a partir da classe TargetMachine caso o backend não fosse desenvolvido para utilizar a Framework de geração de código do LLVM ou a partir da classe LLVMTargetMachine, que é uma subclasse da classe TargetMachine, para os backends que utilizem a Framework. A implementação do

backend M2up utiliza a Framework do LLVM e por isso a classe foi implementada a

partir da classe LLVMTargetMachine e contêm os métodos para aceder ao conjunto de instruções, registos, informação da stack, etc. A classe M2upTargetMachine (Figura 4.2) foi então criada com este propósito.

(50)

No construtor da classe (Figura 4.3) definem-se algumas características tais como o layout dos dados, tamanho do apontador, endianess.

Figura 4.3-Construtor da classe M2upTargetMachine

4.2 Seleção de instruções

Tal como foi visto no capítulo 2, o processo de seleção de instruções está dividido em diversas fases. Nas próximas secções serão analisadas a implementação das fases mais importantes do processo de seleção de instruções.

4.2.1 Construção do DAG inicial

O primeiro passo do processo de geração de código é transformar o código intermédio LLVM num grafo acíclico direcionado. Para cada instrução do programa LLVM é criado um nodo que são instâncias da classe SDNODE. Cada nodo tem um opcode que indica a operação e também os operandos dessa operação. A maior parte das operações definem apenas um valor, mas alguns nodos podem definir mais do que um valor. Cada valor produzido por um nodo tem um MVT (Machine Value Type) que indica o tipo do resultado. Os grafos contêm dois tipos de valores, os que representam fluxo de dados e os que representam fluxos de controlo. Os primeiros são do tipo inteiro ou vírgula flutuante. Os segundos são representados como ligações em cadeia que são do tipo MVT::Other. Estas ligações permitem definir a ordem entre nodos que têm efeitos secundários (tais como Load, Store, calls, returns…). Todos os nodos deste tipo recebem um token do tipo chain como input e produzem um novo como saída.

A Framework de geração de código, mais especificamente as classes SelectionDAGBuild e TargetLowering, são responsáveis pela construção do DAG inicial.

(51)

Para uma melhor análise das diferentes etapas do processo de seleção de instruções apresentam-se as transformações sofridas pelo bloco entry do código LLVM IR presente na Figura 4.4.

Figura 4.4- Código LLVM IR para uma função recursiva que soma n números a partir do número n

Nesta primeira fase de construção do DAG o LLVM IR da Figura 4.4 é transformado num DAG de instruções ilegais, ou seja instruções que podem não pertencer ao M2up. O DAG inicial para o bloco entry desta função está representado na Figura 4.5. A instrução icmp é transformada no nodo setcc e recebe como parâmetros os valores a comparar, constante 0 e o valor da variável n num registo, e o nodo setgt (set

on greater) que é a condição. O resultado do setcc e a constante -1 são a entrada de um

nodo XOR que pretende negar o valor do setcc. A instrução br é transformada no nodo

brcond. O brcond recebe como operandos a chain do nodo EntryToken, o resultado do

(52)

Figura 4.5- DAG inicial para o bloco entry da função recursiva

4.2.2 Optimização do DAG

Antes da fase de legalização um passo de otimização é efetuado. Este primeiro passo de otimização pretende “limpar” o código. Na Figura 4.6 podemos observar as alterações efetuadas ao DAG da Figura 4.5 pelos passos de otimização. O nodo setgt foi substituído pelo nodo setlt não sendo assim necessária a utilização do XOR com a constante -1 para negar a condição. O conjunto de nodos setcc e brcond é substituído

(53)

pelo nodo br_cc. Os operandos do nodo br_cc são a chain do nodo EntryToken, a condição de salto setlt, os dois valores a comparar e por fim o destino do salto.

Figura 4.6-SelectionDag após a primeira fase de otimização

4.2.3 Legalização do DAG

Na fase de legalização do DAG remove-se as instruções e tipos de dados que não são suportados pela máquina alvo. Para isso é necessário fornecer à framework algumas informações específicas do nosso target tais como:

 Os tipos de dados suportados por cada instrução e para cada tipo de dados qual a classe de registos que deve ser utilizada.

 Expandir ou promover instruções para que possam ser utilizadas pela máquina alvo.

 Como lidar com as instruções que não podem ser transformadas automaticamente.

 Como lidar com as convenções de chamada de uma função.

A classe M2upTargetLowering foi implementada como uma subclasse da classe TargetLowering e fornece estas informações à framework do LLVM.

4.2.3.1 Convenções de chamada

A forma como os argumentos são passados e retornados numa função podem variar de máquina para máquina pois enquanto algumas máquinas determinam um conjunto de

(54)

restrições outras funcionam de forma agnóstica. O M2up não define nenhuma convenção de chamada, mas tendo em conta o reduzido número de registos decidiu-se adotar as seguintes:

 Um argumento do tipo i16 pode ser passado no registo R3.

 Caso os registos não sejam suficientes para passar todos os argumentos, estes serão passados através da stack.

 Os valores de retorno são retornados no registo R5.

Um ficheiro de descrição TableGen foi utilizado para definir as convenções de chamada. Na Figura 4.7 estão definidas as convecções de chamada para o M2up apresentadas acima.

Figura 4.7-Descrição das convenções de chamada definidas para o M2up

O RetCC_M2up define o tipo de valores e qual o registo a utilizar para guardar os valores de retorno de uma função. No CC_M2up define-se que os argumentos do tipo i8 devem ser promovidos para o tipo i16 para poderem ser passados no registo R3 ou através da stack.

Sempre que um valor de retorno ou argumento de uma função passa por este processo de legalização a convenção de chamada apropriada é chamada determinando assim o local de destino para a variável. Para além da localização dos argumentos e valores de retorno é necessário definir o processo de legalização para a chamada e retorno de uma função. Este processo não está implementado num ficheiro de descrição mas sim no M2upTargetLowering. As seguintes funções são chamadas neste processo:

Imagem

Figura 2.2- Retargability do compilador em 3 fases [17]
Figura 2.4-Código LLVM IR gerado a partir das funções em linguagem C da figura 2.3 fonte [20]
Figura 2.5- Sequência de passos de geração de código.
Figura 2.7-Classe base Register fornecida pela framework de geração de código do LLVM
+7

Referências

Documentos relacionados

A combinação dessas dimensões resulta em quatro classes de abordagem comunicativa, que podem ser exemplificadas da seguinte forma: interativo/dialógico: professor e

O termo extrusão do núcleo pulposo aguda e não compressiva (Enpanc) é usado aqui, pois descreve as principais características da doença e ajuda a

O valor da reputação dos pseudônimos é igual a 0,8 devido aos fal- sos positivos do mecanismo auxiliar, que acabam por fazer com que a reputação mesmo dos usuários que enviam

Principais mudanças na PNAB 2017  Estratégia Saúde da Família/Equipe de Atenção Básica  Agentes Comunitários de Saúde  Integração da AB e Vigilância 

Apesar dos esforços para reduzir os níveis de emissão de poluentes ao longo das últimas décadas na região da cidade de Cubatão, as concentrações dos poluentes

Identificar a língua espanhola como instrumento de acesso a informações, a outras culturas e grupos sociais com foco na área de Recursos Humanos.. Aplicar

Nessa situação temos claramente a relação de tecnovívio apresentado por Dubatti (2012) operando, visto que nessa experiência ambos os atores tra- çam um diálogo que não se dá

Caramelli e Viel (2006) dizem que o idoso pode começar a apresentar sinais de demência já no início deste novo ciclo, porém, estudos sugerem que a partir dos