José Miguel Pereira da Silva -...

96
José Miguel Pereira da Silva Porting do compilador LLVM com o frontend Clang para uma nova arquitetura de processador José Miguel Pereira da Silva outubro de 2013 UMinho | 2013 Porting do compilador LLVM com o frontend Clang para uma nova arquitetura de processador Universidade do Minho Escola de Engenharia

Transcript of José Miguel Pereira da Silva -...

José Miguel Pereira da Silva

Porting do compilador LLVM com o frontendClang para uma nova arquitetura deprocessador

José

Migu

el Pe

reira

da

Silva

outubro de 2013UMin

ho |

201

3Po

rtin

g do

com

pila

dor

LLVM

com

o fr

onte

nd C

lang

par

a um

a no

va a

rqui

tetu

ra d

e pr

oces

sado

r

Universidade do MinhoEscola de Engenharia

outubro de 2013

Tese de MestradoCiclo de Estudos Integrados Conducentes ao Grau deMestre em Engenharia Eletrónica Industrial e Computadores

Trabalho efetuado sob a orientação doProfessor Doutor João Monteiro

José Miguel Pereira da Silva

Porting do compilador LLVM com o frontendClang para uma nova arquitetura deprocessador

Universidade do MinhoEscola de Engenharia

iii

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.

iv

v

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.

Palvras-Chave: LLVM, Compilador, Backend, Microprocessadores

vi

vii

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.

Keywords: LLVM, Compiler, Backend, Microprocessors.

viii

ix

Í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

4.4 Alocação de registos ........................................................................................ 48

x

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

APÊNDICE A................................................................................................................. 69

xi

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

VLIW- Very Long Instruction Word

xii

xiii

Í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

Figura 4.12-Definição do operando simm4 .................................................................... 42

xiv

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

xv

Índice de Tabelas

Tabela 3.1-Conjunto de Registos.................................................................................... 22

xvi

1

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,

2

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

como: Alpha, SPARC, MIPS e x86.

3

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

4

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.

5

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

6

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).

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

7

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.

O clang é muito mais rápido e utiliza menos memória que o gcc [19].

8

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]

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

9

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;

Classes que modelam o código independentemente do alvo;

10

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.

11

Selecção de

InstruçõesEscalonamento

Optimizações na forma

SSA

Alocação de Registos

Inserção do prólogo e

epílogoOtimizaçõ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

12

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

13

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.

14

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.

15

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).

16

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

Munch ao contrário do algoritmo dinâmico, é topdown. Começando na raiz da árvore

17

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

18

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

19

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.

20

21

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.

22

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

23

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).

24

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.

25

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.

26

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.

27

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.

28

29

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.

30

Figura 4.1- Diagrama de classes simplificada do backend LLVM do M2up. A vermelho as classes base da

31

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.

Figura 4.2- Implementação da classe M2upTargetMachine

32

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.

33

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

XOR e o endereço de destino do salto ( bloco if.end).

34

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

35

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

36

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:

37

LowerCall( ): Quando uma função é chamada, a função LowerCall() é

utilizada. Primeiro é calculado o tamanho da stack necessário para os

argumentos da função. Este tamanho é definido como um argumento da

pseudo-instrução CALLSEQ_START que indica o início da sequência de

chamada. Em seguida uma sequência de STORES são inseridos para

guardar os argumentos na stack. A seguir aos STORES a instrução CALL

é então inserida. Finalmente é inserida a pseudo-instrução

CALLSEQ_END para indicar o fim da sequência.

LowerFormalArguments( ): Quando entramos no scope de uma função é

necessário que esta possa aceder aos argumentos. Para isso é necessário

passar os argumentos para registos virtuais. Os argumentos que já estejam

em registos são copiados para registos virtuais. Os argumentos que estão

na stack são passados para os registos virtuais através da inserção de uma

sequência de LOADs no DAG.

LowerReturn( ): O método LowerReturn() é utilizado na saída do scope

de uma função. O valor de retorno caso exista é copiado para o registo

físico definido nas convenções de chamada e um nodo RET_FLAG é

emitido que mais tarde na seleção de instruções será substituído por um

salto para o valor guardado no registo R7.

4.2.3.2 Instruções ilegais

Existem algumas operações e operandos que têm de ser transformados

manualmente devido a algumas características específicas. Neste passo estes nodos

serão substituídos por nodos intermédios que podem ser utilizados no processo de

seleção de instruções

Saltos condicionais: No código intermédio LLVM uma instrução de

salto condicional é sempre precedida por uma instrução de comparação. A

linguagem LLVM IR utiliza uma instrução icmp para comparar dois números

inteiros. Esta instrução recebe 3 operandos: uma condição para indicar o tipo

de comparação a efetuar e os 2 valores a comparar. A mesma retorna um valor

booleano indicando se a condição comparada é verdadeira ou falsa. Este valor

verdadeiro ou falso é depois utilizado para decidir se o salto condicional deve

38

ser efetuado ou não. Como foi visto nas secções anteriores estas duas

instruções são inicialmente substituídas no Dag pela sequência de nodos setcc

brcond. O M2up não possuiu uma instrução de comparação e o salto

condicional recebe apenas a condição de salto e o endereço de destino. Na fase

de otimização os nodos setcc e brcond são substituídos pelo nodo br_cc. Este

salto condicional já é mais próximo do salto condicional do M2up pois recebe

como operando a condição de salto, no entanto ao contrário do salto do M2up o

br_cc efetua a comparação entre os valores. O M2up não possui uma instrução

de comparação mas possui um registo PSW que contêm as flags Carry, Zero,

Equal, Greater e Less. Estas flags podem ser afetadas por todas as instruções

aritméticas e lógicas. As instruções SUB e SUBi foram utilizadas para

comparar valores ativando assim as flags corretas antes de um salto

condicional. O nodo br_cc é assim substituído por um nodo um nodo

intermédio M2upISD::CMPPSW que na fase de instruções poderá ser

substituído pelas instruções SUB ou SUBi seguida de um nodo intermédio

M2upISD::BR_CC que recebe uma flag correspondente á condição de

comparação e que na fase de seleção de instruções será substituído pelo salto

condicional correspondente. O M2up possuí 5 saltos condicionais, um para

cada flag sendo que o salto é efetuado caso a flag correspondente esteja ativa.

O LLVM possuí condições de comparação para a qual o M2up não saltos

condicionais como por exemplo a condição GE(greater equal). Estas condições

levavam a que fosse necessário incluir 2 saltos condicionais, para o caso do GE

um com a flag Greater e outro com a flag equal pois os saltos M2up aceitam

apenas uma das flags de cada vez. Nestes casos é utilizada a instrução MPSW

do M2up fazendo o toogle á flag oposta, no caso do GE a flag Less alterando-

se assim o salto para um Branch Less em vez de um Branch Greater Equal que

não existe no M2up.

Deslocamentos: O LLVM possui três instruções de deslocamento de

bits, o SHL (Shift Left), o ASHR (Arithmetic shift right) e o LSHR (Logical

Shift Right). O M2up possuí apenas Rotates: RL (Rotate Left), RR (Rotate

Right) , RLC (Rotate Left with Carry) e RRC(Rotate Right with Carry). É

necessário por isso transformar os shifts em rotates:

39

o SHL: o SHL de um pode ser transformado num RLC de

um desde que a flag Carry esteja a zero. O SHL é assim

substituído por uma instrução MPSW para colocar a flag

Carry a 0 e por um RLC. Quando pretendemos

deslocamentos superiores a um é necessário introduzir um

ciclo para introduzir o conjunto MPSW e RLC n vezes

sendo n o número de vezes que se pretende deslocar um

número à esquerda.

o LSHR: o LSHR de um pode ser transformado num RRC

de um desde que a flag Carry esteja a zero. O LSHR é

assim substituído por uma instrução MPSW para colocar a

flag Carry a 0 e por um RLC. Quando pretendemos

deslocamentos superiores a um é necessário introduzir um

ciclo para introduzir o conjunto MPSW e RLC n vezes

sendo n o número de vezes que se pretende deslocar um

número à direita.

o ASHR: o ASHR de um pode ser transformado num RRC

de um desde que a flag Carry seja uma cópia do bit de

sinal. O ASHR é assim substituído por uma instrução

MPSW para colocar a flag Carry com o sinal do número

que pretendemos deslocar e por um RRC. Quando

pretendemos deslocar mais que uma vez é necessário

introduzir um ciclo para introduzir o conjunto MPSW e

RRC n vezes sendo n o número de vezes que se pretende

deslocar um número à direita.

O DAG otimizado mas com instruções ilegais da Figura 4.6 é transformado no

DAG apresentado na Figura 4.8 na fase de Legalização do DAG.

40

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

O nodo ilegal br_cc foi substituído pelos nodos intermédios

M2upISD::CMPPSW e M2upISD:BR_CC que na fase de seleção de instruções no DAG

já podem ser substituídos por instruções nativas do M2up.

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

Nesta fase os nodos do DAG serão então transformados em instruções específicas

do M2up. O gerador de código precisa por isso de informações detalhadas sobre o

conjunto de instruções da máquina alvo. A descrição do conjunto de instruções é feita

recorrendo a dois ficheiros de descrição Tablegen, o M2upInstrFormats.td no qual é

descrito o formato das instruções e o M2upInstrInfo.td que contém a descrição concreta

das diversas instruções.

41

4.2.4.1 Ficheiro de descrição do formato das Instruções

A partir da classe Instruction (Figura 2.6) é definida uma classe M2upInst para

definir as instruções do M2up (Figura 4.9).

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

Como pode ser observado na figura o opcode é definido como sendo os 5 bits mais

significativos das instruções do M2up. A partir desta classe foi implementada uma

classe para cada formato de instrução diferente. Por exemplo, para as instruções

aritméticas e lógicas foi implementada a classe ArithLogic (Figura 4.10).

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

Define-se assim quantos operandos têm as instruções, qual o tamanho dos diversos

operandos e a sua localização no formato da instrução, por exemplo nas instruções

42

aritméticas e lógicas o operando de saída é o rd que tem 3 bits e se encontra entre o bit 8

e 10.

4.2.4.2 Ficheiro de descrição do conjunto de Instruções

O ficheiro M2upInstrInfo.td descreve as instruções do M2up detalhadamente. Este

ficheiro parte das informações recolhidas do ficheiro de formatos descrito anteriormente.

Para explicar como se processa a descrição de uma instrução recorreu~se ao exemplo da

instrução ANDI. A instrução ANDI realiza um AND lógico entre um registo e um

imediato de 4 bits guardando o resultado num registo.

Figura 4.11-Formato da instrução ANDI

A Figura 4.11 mostra o formato da instrução ANDI. Para definirmos esta instrução

temos primeiro de definir os seus operandos. Rd e Rx são registos e por isso estão

definidos nos ficheiros de descrição de registos. Para o imediato de 4 bits foi definido o

operando simm4 (Figura 4.12).

Figura 4.12-Definição do operando simm4

Após a definição dos operandos pode ser definida a instrução. Para facilitar a

codificação e reduzir a repetição no código o TableGen permite agrupar as instruções

em classes e multiclasses. Para as operações aritméticas e lógicas foi definida uma

multiclasse com duas classes (Figura 4.13). A primeira para definir as instruções que

não utilizam imediatos e a segunda as que utilizam imediatos. Esta multiclasse tem

como argumentos o opcode, a string com o nome da instrução, o OpNode que é o

pattern da instrução que terá de ser igualado para que a instrução seja selecionada e o

último argumento é o tipo de itinerário da instrução para ser utilizado no escalonamento

das instruções.

43

Figura 4.13-Definição da multiclasse ArithandLogic

A instrução ADDI pertence á classe que utiliza o formato ArithLogicI definido no

ficheiro de formatos das instruções. Como podemos observar na figura 4.12 a saída das

instruções desta classe é um registo e as entradas um registo e um imediato. É também

definido como se deve formar a string que será enviada para o assembly printer. Entre

parênteses retos define-se a operação da instrução, no caso das operações aritméticas e

lógicas faz-se um set ao registo de saída rd com o valor do resultado da operação

definida no Opnode entre um registo rx e um imediato de 4 bits imm4.

Para definir agora a instrução ADDI é necessário então indicar opcode, o nome da

instrução, o OpNode e o tipo de itinerário:

Como o ANDI está definido numa multiclasse utiliza-se a diretiva defm permitindo

assim definir logo a instrução AND e ANDI.

Este processo de descrição é repetido para as restantes instruções do M2up.

4.2.4.3 Pattern Matching

Após a definição do conjunto de instruções pode então ser efetuado o matching

entre os nodos do DAG e os patterns definidos para cada uma das instruções.

O processo de pattern matching segue os seguintes passos:

1. A Framework de geração de código chama o método genérico

SelectionDAGISel::DoInstructionSelection responsável por chamar o

selecionador de instruções para o target que se pretende utilizar. O

método M2upDagToDagIsel::Select é então chamado pelo método

anterior para analisar e efetuar a seleção de instrução para cada nodo do

DAG.

2. O método M2upDagToDagIsel::Select não efetua no caso do M2up

nenhum matching manual chamando então o método

44

M2upDagToDagIsel::SelectCode que será responsável por todo o

processo de seleção.

3. O método M2upDagToDagIsel::SelectCode é gerado a partir dos

ficheiros de descrição e contêm a tabela de correspondência.

O método SelectCode (Figura 4.14) gerado automaticamente pelo TableGen a

partir dos ficheiros de descrição é um enorme switch case que vai testando para cada

nodo qual a sua correspondência. Para cada um dos nodos pode existir um leque de

instruções possíveis sendo por isso necessário testar cada uma delas por ordem até se

encontrar a correspondência correta. No caso de existir mais do que uma

correspondência correta será substituído pelo primeiro que for encontrado.

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

Para o código LLVM IR da figura 4.5 o processo de pattern matching é o

seguinte:

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

Na seleção de instruções da Figura 4.15 a correspondência é sempre feita à

primeira pois os nodos deste bloco apresentam apenas uma correspondência possível.

Os índices da figura referem-se aos índices que aparecem a comentário no SelectCode.

45

Na Figura 4.16 podemos observar que o índice do nodo M2upISD::CMPPSW é o 591 e

que o único nodo correspondente é o M2up::SUBCCi.

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

Após o processo de seleção de instruções o DAG legalizado da Figura 4.8 é

transformado no DAG apresentado na Figura 4.17.

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

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

46

Os ficheiros de descrição permitem descrever o conjunto de instruções da

máquina alvo. Em adição as estas informações foram implementados um conjunto de

métodos da classe M2upInstrInfo que analisam, transformam e inserem algumas

instruções. Estes métodos são apenas chamados apenas na parte final da geração de

código e não na fase de seleção de instruções. Os métodos implementados foram os

seguintes:

isLoadFromStackSlot: Caso a instrução seja um load direto da stack esta

função retorna o número do registo de destino e o índice da stack.

isStoreToStackSlot: Caso a instrução seja um store direto para a stack

esta função retorna o número registo de origem e o índice da stack.

AnalyzeBranch: Este método analisa os saltos condicionais e

incondicionais removendo ou inserindo saltos de forma a otimizar o

código. Por exemplo se existirem dois saltos incondicionais seguidos o

segundo pode ser removido pois nunca será executado. Caso exista um

salto condicional seguido de um salto incondicional em que o destino do

salto incondicional é o mesmo que o do salto condicional caso este seja

falso o salto incondicional pode ser removido.

InsertBranch: método utilizado pelo AnalyzeBranch para inserir saltos.

RemoveBranch: método utilizado pelo AnalyzeBranch para remover

saltos.

copyPhysReg: copia o valor de um registo físico para outro registo físico.

storeRegToStackSlot: guarda o valor de um registo num endereço da

stack.

loadRegFromStackSlot: retorna para um registo um valor guardado num

endereço da stack.

4.3 Escalonamento de instruções

Os ficheiros M2upSchedule.td e o M2upSchedule4.td foram implementados a

partir dos ficheiros TargetSchedule.td e TargetItinerary.td referidos no capítulo 2. Estes

ficheiros permitem ao LLVM ter uma noção de quais são as unidades funcionais do

micro, a latência e que instruções ocupam quais unidades funcionais indicando assim ao

47

scheduler que estratégia deve adotar para evitar que a latência das unidades funcionais

impeça o escalonamento correto das instruções.

O M2up possuí um pipeline de 5 estágios. No ficheiro M2upSchedule.td são

definidas as unidades funcionais e as classes de itinerários para os diversos tipos de

instrução (figura 26). As unidades funcionais do M2up são o IF (fetch), ID (decode), EX

(execution), MA (memory access) e WB (Write-back).

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

M2upSchedule.td

No ficheiro M2upSchedule4.td define-se qual o itinerário para cada uma das

classes de itinerários definidas no M2upSchedule.td. O itinerário de uma instrução

indica quais as unidades funcionais que utiliza e a latência de cada uma assim como o

número de ciclos necessários para ter os operandos e resultados da instrução disponíveis.

A Figura 4.19 contem a descrição do itinerário das instruções aritméticas e lógicas

indicando assim ao scheduler LLVM que estas instruções não acedem à unidade

funcional de acesso á memória e que demoram apenas um ciclo em cada uma das outras

unidades funcionais. Informa também que o resultado da operação é calculado ao fim de

2 ciclos e que os dois operandos estão disponíveis ao fim de um ciclo.

48

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

Até este ponto o código ainda está representado na forma de DAG. O último

passo do scheduler é transformar o DAG numa lista de instruções. Para isso o scheduler

utiliza o método InstrEmitter::EmitMachineNode para traduzir as instruções a partir do

SDNode. As instruções passam a ter o formato MachineInstr e o DAG pode ser então

destruído. O DAG da Figura 4.17 é transformado na lista da Figura 4.20.

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

4.4 Alocação de registos

O gerador de código do LLVM é responsável pela alocação de registos tal como

foi visto no capítulo 2. No entanto é necessário fornecer ao LLVM informações

detalhadas sobre o conjunto de registos da arquitetura alvo para que a alocação de

registos seja feita corretamente. Grande parte da informação é descrita recorrendo a um

ficheiro de descrição que utiliza a ferramenta TableGen referida no capítulo 2.

4.4.1 Ficheiro de descrição do conjunto de registos

O gerador de código LLVM fornece a classe Register (Figura 2.7) a partir da

qual podem ser implementadas as classes de registos do M2up. A partir desta classe

foram implementadas duas classes para os registos do M2up no ficheiro de descrição de

registos como pode ser observada na Figura 4.21.

49

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

A classe M2upSPRReg é utilizada para podermos definir o registo PSW e a classe

M2upReg é utilizada para definir os outros registos. O parâmetro field bits Num desta

classe permite identificar os registos desta classe numericamente. Cada registo físico

deve então ser definido como uma instância das classes acima.

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

Como pode ser observado na Figura 4.22, o nome e número do registo são passados

como argumentos.

Para terminar o ficheiro de descrição dos registos é necessário definir um conjunto

de register classes. Cada register class é definida por um tipo, um alinhamento e o

conjunto de registos que lhe pertencem. Estas classes de registos são utilizadas no

processo de seleção de instruções pois todas as instruções que utilizem registos como

operandos devem definir estes operandos como sendo de uma register class específica.

Figura 4.23-Definição da RegisterClass Reg_bank que contêm os Registos R0 até R7

Os registos do M2up são portanto agrupados numa classe denominada por

Reg_Bank, indicando que são todos eles registos de 16 bits de inteiros. O registo PSW

50

não foi colocado em nenhuma register classe porque apesar do seu valor poder ser

alterado por uma instrução, ele nunca é um operando de uma instrução.

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

A partir do ficheiro de descrição dos registos o Tablegen gera automaticamente

um conjunto de informações sobre os registos da máquina alvo. No entanto algumas

características dos registos têm de ser determinadas em runtime. Estas informações

foram codificadas em c++ no M2upRegisterInfo.cpp. Algumas das informações

necessárias são:

Registos reservados: O M2upRegisterInfo contém um método que marca

todos os registos reservados num vetor de bits. O Registo R0 retorna

sempre zero e por isso é um registo reservado. O Registo R7 guarda o

valor de retorno após um salto. O M2up não define nenhum registo como

stack pointer por isso o autor optou por definir o R6 como stack pointer.

Registos Callee-Saved: Normalmente o ABI (Application Binary

Interface) define um conjunto de registos que devem ser guardados na

entrada e retorno de uma função.

O registo R7 que guarda o endereço de retorno de uma função deve em

alguns casos ser guardado no início de uma função e restaurado na saída

da função e por isso está definido como registo Callee-Saved. Todos os

outros registos estão definidos como Caller-Saved.

Frame Register: O frame register é um endereço base para todos os

acessos á stack. Na maior parte dos casos, o tamanho da frame é fixo e por

isso os endereços da stack são calculados a partir do stack pointer (R6).

No caso de o tamanho da frame ser variável (por exemplo se for utilizada

uma função de alocação) é necessário utilizar o frame pointer.

No ficheiro M2upRegisterInfo.cpp foram também implementados alguns métodos

que emitem fragmentos de código. Estes métodos são chamados numa fase final da

geração de código onde os processos de seleção e escalonamento de instruções assim

como a alocação de registos já terminou. Os métodos implementados são então:

51

eliminateFrameIndex( ): Antes da chamada desta função, o gerador de

código endereça a stack através de um frame índex abstrato e de um

imediato. Esta função é chamada sempre que encontra uma instrução que

aceda á stack para substituir o endereço por um registo e por um Offset

real. Este registo tanto pode ser o stack pointer como o frame pointer

conforme a função tenha um stack frame fixo ou variável. Quando uma

função está a aceder a um dado que está fora do seu espaço de stack o

offset abstrato vem com o valor negativo e por isso é necessário adicionar

a este offset o tamanho da stack da função para obtermos o offset real. As

instruções de STORE e LOAD com registos e imediatos incrementam

automaticamente os registos com os valores dos imediatos. Estas

instruções não podem por isso ser utilizadas pois o valor do stack pointer

ou frame pointer iria alterar sempre que um elemento da stack fosse

acedido, tornando assim impossível o cálculo dos offsets corretos. O valor

do Offset é então passado para um Registo através da Instrução LDI e

depois é emitida uma instrução de STORE/LOAD que só tem registos

como operandos.

eliminateCallFramePseudoInstr( ): Sempre que uma instrução de CALL

é emitida, as pseudo-instruções ADJCALLSTACKDOWN e

ADJCALLSTACKUP são emitidas respetivamente antes e depois do

CALL. Se a função que estiver a ser chamada tiver uma stack frame fixa

estas pseudo-instruções são removidas porque o espaço para todos os

argumentos já foi alocado no prólogo da função. Caso a stack frame

contenha objetos de dimensão variável estas funções são substituídas por

adições/subtrações ao stack pointer.

Após a alocação de registos o código para o bloco entry da função recursiva do

código LLVM IR da figura Figura 4.4 é o seguinte:

Figura 4.24- Código para o bloco entry após o passo de alocação de registos

52

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

Neste passo é inserido o prólogo e o epílogo da função. A informação sobre o

prólogo e o epílogo está no ficheiro M2upTargetFrameLowering que contém também

informações sobre a organização da stack tais como alinhamento e direção. O ficheiro

M2upTargetFrameLowering contêm alguns métodos que são chamados imediatamente

antes dos métodos eliminateFrameIndex() e eliminateCallFramePseudoInstr() referidos

anteriormente.

Os métodos implementados são:

EmitPrologue( ): A função EmitPrologue é utilizada para inserir o

prólogo no inicio das funções. No prólogo da função é necessário

reservar o espaço para a stack frame da função. Se não for necessária a

utilização do frame pointer basta adicionar ao stack pointer o tamanho da

stack frame. Como a instrução ADDi apenas permite adicionar imediatos

de 4bits se o tamanho da stack frame for superior a 8 bytes é necessário

emitir uma instrução LDI para guardar o tamanho da stack num registo e

depois uma instrução ADD para somar ao stack pointer o registo que

contêm o tamanho da stack. Se for necessária a utilização do frame

pointer o frame pointer toma o valor do stack pointer anterior.

EmitEpilogue( ): é utilizada para inserir o epílogo das funções. Faz o

processo inverso ao EmitPrologue libertando o espaço reservado para a

stack frame restaurando os valores dos registos stack pointer e frame

pointer. A instrução SUBi tal como a ADDI utiliza imediatos de 4 bits

sendo por isso necessário usar um par LDI seguido de um SUB para

restaurar os valores dos registos caso o tamanho da stack frame seja

superior a 8 bytes.

spillCalleeSavedRegisters( ): Esta função é utilizada para fazer o spill

dos registos callee-saved. No caso do M2up o único registo definido

como callee saved foi o R7 que guarda o endereço de retorno após um

salto. Esta função permite então guardar o endereço de retorno no

prólogo da função e retornar o endereço no epílogo quando necessário.

Após a emissão do prólogo e epílogo o código para o bloco entry do código

LLVM IR da Figura 4.4 é o seguinte:

53

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

4.6 Passos personalizados

O LLVM permite adicionar novos passos à geração de código de cada um dos

targets. Foi implementado um passo específico ao M2up para lidar com saltos cujo

destino não seja possível endereçar através de alguns saltos disponíveis pelo M2up. O

M2up apresenta dois formatos para os saltos incondicionais, um relativo ao PC e outro

relativo a um registo. Na fase de seleção de instruções os saltos incondicionais do

LLVM são substituídos pelo salto relativo ao PC do M2up (exceto no caso do return de

uma função em que o salto é relativo ao registo R7 que guarda o endereço de retorno de

uma função). O salto relativo ao PC utiliza o valor do PC mais um imediato de 10 bits o

que pode não ser suficiente para endereçar toda a memória de código. O endereço do

salto é calculado neste passo e caso não seja possível utilizar o endereçamento

relativamente ao PC o salto é substituído por um salto relativo a um registo que tendo

16 bits permite endereçar toda a área de memória. Os saltos condicionais têm apenas

endereçamento relativamente ao PC mas têm um alcance ainda mais reduzido que os

saltos incondicionais pois o valor do salto é dado pelo PC mais um imediato de 8 bits.

Os saltos condicionais, no caso de não ser possível endereçar o endereço de memória

pretendido, são substituídos pelo salto condicional oposto e por um salto incondicional

com endereço relativo a um registo. O endereço do salto condicional passa a ser a

instrução imediatamente a seguir ao salto incondicional e o endereço do salto

condicional original passa a ser o endereço do salto incondicional.

4.7 Emissão de código

A emissão de código na forma assembly é o passo final da geração de código. O

processo de emissão do código assembly é bastante direto. Para cada função existente

será chamado o método RunOnMachineFunction( ). Este método imprime o header da

54

função e de seguida processa os diferentes blocos da função. As instruções de cada

bloco serão emitidas recorrendo ao método printInstruction(). Este método é gerado

automaticamente a partir do ficheiro de descrição de descrição das instruções no qual

está definido qual a string assembly para cada uma das instruções. Os operandos das

instruções são quase todos emitidos pelo método printOperand( ) tirando os operandos

de memória registo mais imediato e as flags dos saltos condicionais. Para lidar com

estes operandos foram criados os métodos printMemOperand( ) e printCCOperand( ) na

classe M2upAsmPrinter (figura 32).

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

4.8 Integração do novo backend no LLVM

Finalmente, após a implementação do backend é necessário alterar alguns

ficheiros do sistema LLVM para que o backend possa ser usado.

O LLVM inclui ficheiros de configuração para dois sistemas de montagem, GNU

Autotools e o CMake. A diretoria do M2up foi introduzida nos ficheiros de

55

configuração para que este possa ser compilado juntamente com os outros componentes

do LLVM.

Para que o LLVM reconheça o target foi necessário registá-lo utilizando o

TargetRegistry. O TargetRegistry permite às ferramentas do LLVM encontrarem e

utilizarem o nosso target em runtime.

Figura 4.27- Registo do M2up como Target do LLVM

O LLVM define um string target triple para identificar as diferentes plataformas.

Esta string consiste em três partes: arquitetura, vendedor e sistema operativo. Todas as

arquiteturas válidas estão listadas através de uma enumeração. O M2up foi adicionado

às arquiteturas válidas. Quando for necessário selecionar o backend apropriado para um

determinado target triple (Triple::m2up no caso do M2up) o LLVM tentará encontrar

uma correspondência com os targets registados através do TargetRegistry.

Por fim é necessário fazer a integração com o frontend Clang. Idealmente este

passo não devia ser necessário pois o frontend deveria ser completamente independente

do target. Tal não se sucede para as linguagens C e C++ pois os tamanhos dos tipos de

dados variam de plataforma para plataforma. Foi por isso necessário adicionar ao clang

informações sobre os tamanhos dos diferentes tipos de dados do M2up para que este

possa ser suportado pelo frontend.

4.9 Resumo

O processo de geração de código do LLVM está dividido em diversas fases, a

seleção de instruções, o escalonamento de instruções, alocação de registos, emissão do

epílogo e do prólogo e emissão de código. A Framework de geração de código fornece

um conjunto de algoritmos para a implementação destas diferentes fases que necessitam

no entanto de informações específicas sobre a máquina alvo o M2up. Estas informações

podem ser fornecidas através de ficheiros de descrição que utilizam a ferramenta

TableGen ou através da implementação de um conjunto de métodos específicos ao

M2up.

56

O processo de seleção de instruções é o passo mais importante da geração de

código. O código LLVM IR é inicialmente transformado num DAG que pode conter

instruções e tipos de dados ilegais. Este DAG sofre depois um conjunto de alterações

até chegar a um DAG que contem apenas instruções nativas e tipos de dados suportados

pelo M2up.

No capítulo 5 apresentam-se os testes efetuados ao backend M2up analisando-se

os resultados obtidos.

57

5 Testes e Resultados

Neste capítulo será feito um estudo dos resultados obtidos por um grupo de testes

efetuados ao conjunto Clang e backend M2up.

5.1 Backend M2up

O backend M2up possui algumas limitações mas em conjunto com o Clang é capaz

de compilar um grande conjunto de códigos C e C++. As limitações do backend M2up

prende-se sobretudo com os tipos de dados que o processador suporta:

As operações aritméticas e lógicas podem utilizar dados do tipo i8 (são

estendidos para i16) ou i16 mas não suportam dados do tipo i32 ou i64.

Variáveis do tipo float também não são suportadas.

O backend M2up suporta no entanto todo o conjunto de instruções do processador

M2up nomeadamente as instruções de controlo de fluxo como os saltos condicionais e

incondicionais que são utilizados para a chamada e retorno de uma função ou nos

tradicionais ciclos e testes de condição das linguagens C e C++.

5.2 Testes e Resultados Obtidos na Simulação

O conjunto de testes efetuados não foi muito extenso. Para cada teste são

apresentados os resultados da compilação (frontend e backend) e os resultados obtidos

na simulação do micro processador. Nesta primeira versão do compilador os testes

foram apenas efetuados na simulação do microcontrolador com o objetivo de validar a

sua implementação. O primeiro teste efetuado foi um teste simples em que se faz a soma

de dois números. Na Figura 5.1 está representado o código C para este teste.

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

58

O Clang foi utilizado como frontend para gerar o código intermédio LLVM. Na

Figura 5.2 apresenta-se o código LLVM IR gerado pelo Clang a partir do código C da

Figura 5.1.

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

Utilizando o gerador de código do LLVM definindo como backend o M2up o

código LLVM IR foi transformado no código assembly da Figura 5.3.

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

O assemblador do M2up foi utilizado para gerar o código binário a partir do

código assembly para se efetuar o teste na simulação do micro.

59

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.

Na Figura 5.4 podem ser observados os resultados da simulação deste primeiro

teste. Nesta simulação podem ser observados os valores dos opcodes das instruções que

foram executadas, o valor dos registos (em decimal com sinal) e as linhas de memória

(em hexadecimal) que são afetadas pela execução do teste, o sinal de clock e o sinal de

reset. Os valores das variáveis a, b e c do código da Figura 5.1 são todas guardadas em

memória e verifica-se que o resultado da soma é o correto.

O segundo teste efetuado foi um código para testar o funcionamento dos saltos

condicionais recorrendo para isso a uma condição if-else. O código testado foi o

seguinte:

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

60

Na Figura 5.6 apresenta-se o código LLVM IR gerado pelo Clang a partir do

código C da Figura 5.5.

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

Utilizando o gerador de código do LLVM definindo como backend o M2up o

código LLVM IR foi transformado no código assembly da Figura 5.7.

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

61

O assemblador do M2up foi utilizado para gerar o código binário a partir do

código assembly para se efetuar a simulação. A simulação da Figura 5.8 permite

visualizar os mesmos dados que a simulação do primeiro teste mas acrescenta uma nova

linha com o valor das flags do registo PSW. O código C da Figura 5.5 apresenta apenas

uma variável, a variável e, como pode ser observado na última linha da simulação que

apresenta os dados relativos à memória a variável e tem inicialmente o valor 6.

Analisando o código C percebe-se que no final do programa a variável e deverá ter o

valor 4 porque a condição do if é falsa e observando a linha de memória verifica-se que

isto acontece. O registo R5 contém o valor de retorno da função e como o retorno da

função é o valor da variável e também este fica com o valor 4. A observação do registo

PSW permite perceber que as alterações nas flags foram efetuadas corretamente para

permitir que o salto condicional fosse efetuado corretamente.

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

O terceiro caso de teste, testa as funções e convenções de chamada. O código

utilizado foi o seguinte:

62

Figura 5.9-Código c para do terceiro teste

Na Figura 5.10 apresenta-se o código LLVM IR gerado pelo Clang a partir do

código c da Figura 5.9. Na Figura 5.11 e na Figura 5.12 apresenta-se o código assembly

M2up para as funções main() e soma().

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

63

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

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

O assemblador do M2up foi utilizado para gerar o código binário a partir do

código assembly para se efetuar a simulação. Na Figura 5.13 pode ser observado o

resultado da simulação deste terceiro teste. Pela simulação verifica-se que a chamada da

função soma ocorreu corretamente assim como o retorno para a função main. Os

64

argumentos foram passados corretamente à função soma e verifica-se que o resultado

obtido após a soma dos números é o correto.

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.

5.3 Resumo

Com objetivo de validar a implementação foi realizado um conjunto de testes

simples. Foram realizados três casos de teste, o primeiro testando um código que

somava duas variáveis, o segundo para testar os saltos condicionais recorrendo a uma

condição if-else e o terceiro para testar a chamada e retorno de uma função. Os

resultados da compilação e simulação no micro foram apresentados para cada um dos

testes verificando-se que estes executavam corretamente.

65

6 Conclusão

O principal objetivo desta tese era dotar o microprocessador M2up de um

compilador para as linguagens C e C++. Nesta tese apresentou-se o design,

implementação e teste de um backend LLVM para o M2up.

O desenvolvimento deste backend teve como base a Framework de geração de

código do LLVM. Esta Framework fornece um conjunto de algoritmos para a geração

de código assim como um conjunto de plataformas base a partir das quais foram

implementadas diversas partes do backend M2up.

Um conjunto de informações sobre a plataforma alvo o M2up, foi fornecida à

Framework através de um conjunto de ficheiros de descrição TableGen e também de um

conjunto de métodos específicos para lidar com algumas transformações que ainda não

são possíveis de realizar através dos ficheiros de descrição.

A estrutura modular do LLVM permitiu uma fácil integração do backend M2up na

infraestrutura do LLVM assim como com o frontend Clang. Esta combinação entre o

backend M2up desenvolvido e o Clang permite a geração de código assembly M2up a

partir de código fonte em linguagem C e C++.

6.1 Trabalho futuro

O desenvolvimento do backend M2up no futuro passa sobretudo por um maior

número de testes ao mesmo para encontrar e corrigir os erros eventualmente

encontrados.

Os testes foram efetuados apenas na simulação do M2up mas podem ser usados

numa implementação real do microcontrolador por forma a garantir a validade dos

resultados obtidos em simulação.

O backend M2up foi desenvolvido essencialmente com o objetivo de ser funcional

descuidando nesta sua primeira fase outros fatores. Vários passos de otimização podem

assim ser estudados e acrescentados ao backend melhorando os tempos de compilação,

o tamanho do código produzido, tempo de execução ou consumo de energia.

66

67

7 Bibliografia

[1] [Online]. http://www.artemis-ju.eu/embedded_systems

[2] Christopher W. Frase and David R. Hanson, A Retargetble C Compiler: Design

and Implementation.: The Benjamim/Cummings Publishing Company,Inc., 1995.

[3] [Online]. https://github.com/drh/lcc/blob/master/CPYRIGHT

[4] Andrew S Tanenbaum and E.G. Keizer, J.W. Stevenson H van Staveren, "A

Practical Toolkit for Making Portable Compilers", vol. 26, no. 9, pp. 654-660

CACM, Ed., 1983.

[5] D. Grune and C.J.H. Jacobs, A Programmer-friendly LL(1) Parser Generator, vol.

18 Software - Practice and Experience, Ed., 1988.

[6] http://www.compilers.de/vbcc.html.

[7] Dr. Rainer Leupers, "LANCE: A C Compiler Platform for Embedded Processors,"

University of Dortmund, 44221 Dortmund, Germany, Feb.2001.

[8] [Online]. http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

[9] [Online]. http://www.langpop.com/

[10] [Online]. http://llvm.org/

[11] [Online]. http://llvm.org/releases/2.8/LICENSE.TXT

[12] http://macruby.org/.

[13] http://openjdk.java.net/legal/binary-plugs-2007-05-08.html.

[14] http://icedtea.classpath.org/.

[15] http://icedtea.classpath.org/wiki/ZeroSharkFaq.

[16] [Online]. http://www.gnu.org/licenses/licenses.html

[17] [Online]. http://clang.llvm.org/comparison.html

[18] [Online]. http://llvm.org/devmtg/2007-05/09-Naroff-CFE.pdf

[19] [Online]. http://clang.llvm.org/features.html#performance

[20] [Online]. http://llvm.org/docs/LangRef.html

68

[21] [Online]. http://llvm.org/docs/CodeGenerator.html

[22] A.V.Aho e S.C.Johson, "Optimal Code Generation for Expression Trees," Journal

of the ACM, vol. 23, p. 1976, Julho 1976.

[23] R. G. G. Cattell, "Formalization and Automatic Derivation of Code Generators,"

CARNEGIE-MELLON UNIV PITTSBURGH PA DEPT OF COMPUTER

SCIENCE, 1978.

[24] [Online]. http://llvm.org/devmtg/2007-05/09-Naroff-CFE.pdf

69

Apêndice A

Descrição detalhada do ISA M2up

Table 1.1 ALU instructions

ADD Rd, Rx, Ry

Adds the contents of the registers Rx and Ry and saves the result in the

register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + Ry) > 32767 or (Rx + Ry) < -32768

Carry = 0 -32768 < (Rx + Ry) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

ADDi Rd, Rx, immd

Adds the content of the register Rx and the immediate value given by

the instruction and saves the result in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + immd) > 32767 or (Rx + immd) < -32768

Carry = 0 -32768 < (Rx + immd) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: -8 to 7.

70

SUB Rd, Rx, Ry

Subtracts the contents of the registers Rx and Ry and saves the result

in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + Ry) > 32767 or (Rx + Ry) < -32768

Carry = 0 -32768 < (Rx + Ry) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

SUBi Rd, Rx, immd

Subtracts the content of the register Rx and the immediate value given

by the instruction and saves the result in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx - immd) > 32767 or (Rx - immd) < -32768

Carry = 0 -32768 < (Rx - immd) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: -8 to 7.

AND Rd, Rx, Ry

Performs the logical “and” of the contents of the registers Rx and Ry

and saves the result in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + Ry) > 32767 or (Rx + Ry) < -32768

Carry = 0 -32768 < (Rx + Ry) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

71

ANDi Rd, Rx, immd

Performs the logical “and” of the content of the register Rx and the

immediate value given by the instruction and saves the result in the

register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + immd) > 32767 or (Rx + immd) < -32768

Carry = 0 -32768 < (Rx + immd) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: -8 to 7.

OR Rd, Rx, Ry

Performs the logical “or” of the contents of the registers Rx and Ry

and saves the result in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + Ry) > 32767 or (Rx + Ry) < -32768

Carry = 0 -32768 < (Rx + Ry) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

ORi Rd, Rx, immd

Performs the logical “or” of the content of the register Rx and the

immediate value given by the instruction and saves the result in the

register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + immd) > 32767 or (Rx + immd) < -32768

Carry = 0 -32768 < (Rx + immd) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: -8 to 7.

72

XOR Rd, Rx, Ry

Performs the logical “xor” of the contents of the registers Rx and Ry

and saves the result in the register Rd.

Condition codes in PSW Register: Flag Conditions

Carry = 1 (Rx + Ry) > 32767 or (Rx + Ry) < -32768

Carry = 0 -32768 < (Rx + Ry) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

XORi Rd, Rx, immd

Performs the logical “xor” of the content of the register Rx and the

immediate value given by the instruction and saves the result in the

register Rd.

Condition codes in PSW Register:

Flag Conditions

Carry = 1 (Rx + immd) > 32767 or (Rx + immd) < -32768

Carry = 0 -32768 < (Rx + immd) < 32767

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: -8 to 7.

NOT Rd, Rx

Performs the logical “not” of the content of the registers Rx and saves

the result in the register Rd.

RR Rd, Rx, Ry

Rotates the content of the register Rx to the right by the value of the

content of the register Ry and saves the value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

73

RRi Rd, Rx, immd

Rotates the content of the register Rx to the right by the immediate

value given by the instruction and saves the value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

RRC Rd, Rx, Ry

Rotates the content of the register Rx to the right by the value of the

content of the register Ry through the carry flag (C) and saves the

value in the register Rd.

Condition codes in PSW Register: Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = Ry

Equal = 0 Rx ≠ Ry

Greater = 1 Rx > Ry

Greater = 0 Rx ≤ Ry

Less = 1 Rx < Ry

Less = 0 Rx ≥ Ry

RRCi Rd, Rx, immd

Rotates the content of the register Rx to the right by the immediate

value given by the instruction through the carry flag (C) and saves the

value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

74

RL Rd, Rx, Ry

Rotates the content of the register Rx to the left by the value of the

content of the register Ry and saves the value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

RLi Rd, Rx, immd

Rotates the content of the register Rx to the left by the immediate

value given by the instruction and saves the value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

RLC Rd, Rx, Ry

Rotates the content of the register Rx to the left by the value of the

content of the register Ry through the carry flag (C) and saves the

value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

75

RLCi Rd, Rx, immd

Rotates the content of the register Rx to the left by the immediate

value given by the instruction through the carry flag (C) and saves the

value in the register Rd.

Condition codes in PSW Register:

Flag Conditions

Zero = 1 Rd = 0

Zero = 0 Rd ≠ 0

Equal = 1 Rx = immd

Equal = 0 Rx ≠ immd

Greater = 1 Rx > immd

Greater = 0 Rx ≤ immd

Less = 1 Rx < immd

Less = 0 Rx ≥ immd

Range of immediate value: 0 to 7.

SPSW Bit , Value

Sets or clears the respective bit in the Program Status Word (PSW)

(Carry, Zero, Equal, Greater, Less).

TPSW Bit

Toggles the respective bit in the Program Status Word (PSW) (Carry,

Zero, Equal, Greater, Less).

Table 1.2 Data transfer instructions

LDI Rd, immd

Loads the immediate value given by the instruction to the

register Rd.

Range of immediate value: -128 to 127.

LD Rd, @Rx, @Ry

Loads the content of the memory position addressed by the sum

of the registers Rx and Ry and saves the value in the register Rd.

LDB Rd, @Rx, @Ry

Loads the byte of the memory position addressed by the sum of

the registers Rx and Ry and saves the value in the register Rd.

LD Rd, @Rx+, immd

Loads the content of the memory position addressed by the

content of the register Rx and saves the value in the register Rd.

Then the register Rx is incremented by the immediate value.

Range of immediate value: 0 to 7.

LDB Rd, @Rx+, immd

Loads the byte of the memory position addressed by the content

of the register Rx and saves the value in the register Rd. Then the

register Rx is incremented by the immediate value.

Range of immediate value: 0 to 7.

76

LD Rd, @PC, immd

Loads the content of the memory position addressed by the sum

of the Program Counter and the immediate value given by the

instruction and saves the value in the register Rd.

Range of immediate value: -32 to 31.

LDB Rd, @PC, immd

Loads the byte of the memory position addressed by the sum of

the Program Counter and the immediate value given by the

instruction and saves the value in the register Rd.

Range of immediate value: -32 to 31.

LDSFR SFRn, Rx

Loads the content of the register Rx and saves in the special

function register SFRn. This instruction can only be executed in

privileged mode.

LDPID PIDn, Rx

Loads the content of the register Rx and saves in the PID register

PIDn. This instruction can only be executed in privileged mode.

ST Rd, @Rx, @Ry

Stores the content of the register Rd in the memory position

addressed by the sum of the registers Rx and Ry.

STB Rd, @Rx, @Ry

Stores the least significant 8-bits of the register Rd in the

memory position addressed by the sum of the registers Rx.

ST Rd, @Rx+, immd

Stores the content of the register Rd in the memory position

addressed by the content of the register Rx. Then the register Rx

is incremented by the immediate value.

Range of immediate value: 0 to 7.

STB Rd, @Rx+, immd

Stores the least significant 8-bits of the register Rd in the

memory position addressed by the content of the register Rx.

Then the register Rx is incremented by the immediate value.

Range of immediate value: 0 to 7.

ST Rd, @PC, immd

Stores the content of the register Rd in the memory position

addressed by the sum of the Program Counter and the immediate

value given by the instruction.

Range of immediate value: -32 to 31

77

STB Rd, @PC, immd

Stores the least significant 8-bits of the register Rd in the

memory position addressed by the sum of the Program Counter

and the immediate value given by the instruction.

Range of immediate value: -32 to 31.

STSFR SFRn, Rx

Stores the content of the special function register SFRn and saves

in the register Rx. This instruction can only be executed in

privileged mode.

STPID PIDn, Rx

Stores the content of the PID register PIDn and saves in the

register Rx. This instruction can only be executed in privileged

mode.

78

Table 1.3 Control instructions

JMP @Rd, immd

Jump to the address given by the sum of the register Rd and the

immediate value given from the instruction.

R7 = Return Address.

JMP @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction.

R7 = Return Address.

BRC @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction if the Carry flag is

set.

BRZ @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction if the Zero flag is

set.

BRE @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction if the Equal flag is

set.

BRG @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction if the Greater flag is

set.

BRL @PC, immd

Jump to the address given by the sum of the Program Counter and

the immediate value given by the instruction if the Less flag is

set.

Table 1.4 Miscellaneous instructions

HALT Halts the processor‟s execution

SYSENTER Changes the processor‟s operation mode to privileged mode

RETI Return from interrupt