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