# Compilação e execução de código
# Instruction Set Architecture (ISA)
- Modelo abstrato de uma arquitetura de processador
- Define o conjunto de instruções disponíveis
- Estabelece o funcionamento lógico do processador, e desta forma o interface entre o software e o hardware
- Permite múltiplas realizações (ex: diferentes modelos de processadores da Intel), cada uma com desempenhos diferentes.
- Define as instruções disponíveis-RiscV (opens new window)
- Define os operandos;
- Define a forma como se acede à memória e aos periféricos:
- Instruções dedicadas de acesso à memória (load/store), ou diretamente como um operando de qualquer instrução.
- Barramento dedicado de acesso aos periféricos (instruções específicas) ou diretamente mapeados numa zona de memória.
- Define a forma como se tratam exceções e interrupções
- Interrupção: ocorrência de um sinal externo ao processador (ex: o utilizador pressiona uma tecla do teclado);
- Exceção: ocorrência de um evento especial não programado durante a execução de um programa (ex: divisão por zero)
# Execução do programa
Sistema Operativo:
- Gestão das entradas e saídas;
- Gestão da memória (RAM e disco), incluindo inicialização das estruturas de dados necessárias para começar a executar o programa;
- Escaloamento da execução das tarefas e gestão dos recursos partilhados.
Hardware:
- Processador, memória, controladores I/O.
# RISC-V
- O RISC-V contém:
- 32 registos inteiros (x0,x1,...,x31);
- 32 registos para virgula flutuante (f0,...f31)- se a extensão de floating point estiver ativa
- Dependendo da implementação, os registos podem ser de 32 ou 64 bits.
- O registo x0 vale sempre 0 (mesmo após uma escrita nesse registo)
Por exemplo:
add x10, x12, x13 #soma a com b e coloca em x
add x11, x14, x15 #soma c e d
sub x10, x10, x11 #calcula o resultado (a+b)-(c+d)
# Instruções de acesso à memória
Memória: Conjunto de gavetas númeradas de 00...00h a FF...FFh (endereço).
Operações:
- Load registo, endereço: Leitura do valor em memória, no endereço indicado, e escrita no registo.
- Store registo, endereço: Escrita do valor armazenado no registo em memória (no endereço indicado).
A memória é endereçável ao byte (caso típico). Operandos com dimensões superiores a um byte ocupam várias posições na memória. O alinhamento dos dados em memória é garantido colocando os m bits menos significativos do endereço a zero, onde m é calculado como:
Exemplo de acesso à memória
la x1,0x2a #pseudo-instrução
lb x2,3(x1) # x2 = FFFF FFBBh
lb x13,-3(x1) # x3 = 0000 0053h
lbu x4,3(x1) # 0000 00BBh
sb x4,-1(x1) # guarda os 8 bits menos significativos de x4 no endereço 29h da memória.
Cópia de lista de valores
Considere uma lista de palavras (32 bits), armazenada em memória. Escreva o código que copia os 4 elementos para a zona de outra lista de valores.
Lista1=100h {-1127,+3401,1457,-4832}
Lista2=200h {...}
la x10, lista1
#Na prática diz-se que x10 e x11 são ponteiros para a Lista1 e Lista2 (guardam o endereço do primeiro elemento da lista)
la x11, lista2
lw x12, 0(x10)
sw x12, 0(x11)
lw x12, 4(x10)
sw x12, 4(x11)
lw x12, 8(x10)
sw x12, 8(x11)
lw x12, 12(x10)
sw x12, 12x11)
# Diretivas de Assembler
As diretivas não são instruções, mas sim comandos para o Assembler gerar código binário
Diretivas
.text Declara uma zona de código (instruções)
.data Declara uma zona de dados writable
.zero N Declara um vetor de N bites inicializados a 0
.byte num1[,num2,...] Declara um vetor de bytes (1B) sequenciais em memória com valores num1, [num2,...]
.half num1[,num2,...] Declara um vetor de half (2B) sequenciais de memória
.word num1[,num2,...] Declara um vetor de words (4B) sequenciais de memória
.string "list of characters" Declara uma string em memória. Cada character (char) ocupa 8 bytes.
Por exemplo:
# Declaração de variáveis
.data
var1: .word 153
var2: .half -1227, 3443, 213, 0x14,13
str1: .string "cadeia de caractéres"
# Código
.text
la x1,var1
lw x2,0(x1)
loop: j loop
# Controlo de Fluxo de Instruções
Cada instrução ocupa 4 bytes e, de maneira geral, a primeira está no endereço 0. Por omissão, as operações são executadas por sequência. Para que o processador saiba em que instrução se encontra, existe um registo que contém o endereço da instrução a executar: Program Counter (PC)
Ponto inicial. quando se faz reset ao processador coloca-se o PC=0, obrigando a executar a instrução armazenada na posição 0. Como cada instrução ocupa 4 bytes, o registo PC incrementa 4 cada vez que uma instrução é executada.
Na prática:
- Sempre que uma instrução é executada, o PC tem necessariamente de ser alterado.
- Pode-se dizer que o PC é um ponteiro que aponta (guardar o endereço) para a próxima instrução a executar.
- As instruções são sempre executadas em sequência, sem possibilidades de interromper o fluxo de instruções.
# Chamadas a funções e introdução à pilha
Pilha: Espaço reservado na memória para guardar dados temporários. O acesso a este espaço de memória é feito segundo um política do tipo "Last in, first out", isto é, o último elemento a entrar é o primeiro a sair. Existem duas operações:
- PUSH: Colocar um valor na pilha.
- POP: Remover um valor da pilha.
Geralmente, a pilha é implementada no sentido de enderenços decrescentes. O stack pointer aponta para a última posição ocupada pela pilha.
- Implementar o PUSH:
addi x2, x2, -12
sw x11, 8(x2)
sw x12, 4(x2) #x2 = sp (stack pointer)
sw x13, 0(x2)
- Implementar o POP:
lw x11, 0(x2)
lw x12, 4(x2)
addi x2, x2, 8
Exemplo de utilização da pilha:
Considerando o exemplo em baixo em C, vamos transformá-lo em assembly usando a pilha
int leaf_example(int g, int i, int j){
int f;
f = (g+h) - (i+j);
return f;
}
Vamos considerar que: g -> x10 h -> x11 i -> x12 j -> x13 f -> x20
.text
#Abrir a pilha, neste caso em particular abrimos 3 espeços na pilha
(1 para cada operação)
addi sp, sp, -12
sw x5, 8(sp)
sw x6, 4(sp)
sw x20, 0(sp)
add x5, x10, x11 #g+h
add x6, x12, x13 #i+j
sub x20, x5, x6 #(g+h) - (i+j)
addi x10, x20, 0 #return de f
#Fechar a pilha
lw x20, 0(sp)
lw x6, 4(sp)
lw x5, 8(sp)
addi sp, sp, 12
jalr x0, 0(x1)
# Código de chamada de uma função
- Passagem de parâmetros de entrada (Alternativa a este método é a utilização de uma pilha);
- Chamada da função - Existe uma instrução específica:
jal x1, function (jal: jump and link)
Esta função realiza:
- Um salto (Jump) para a função (Coloca o PC a apontar para a primeira instrução da função indicada).
- Guarda (Link) no registo x1 o endereço de retorno.
- Por convenção do RISC-V, o registo de link é o x1.
- Leitura dos parâmetros de entrada
- Execução do traço de código.
- Retorno do resultado
Como a chamada à função realizou a operação de Link (salvaguarda do endereço de retorno), o retorno pode ser simplesmente realizado através da instrução:
jalr x0, x1 ou ret
- Retorno do fluxo de execução para o ponto original.
- Leitura do resultado.
# Suporte do código para uma função
Em geral, a passagem de parâmetros e o retorno do resultado é realizado por registos, desde que haja registos em número suficiente. Se não houver, deve-se usar a pilha.
Nesse caso, os parâmetros devem ser colocados na pilha antes da chamada à função, e retirados da pilha após a função.
Adicionalmente, deve-se evitar que a função chamada altere os registos usados pela função/código base. Assim deve-se;
- Salvaguardar o contexto: Guardar na pilha os valores originais de todos os registos que vão ser alterados pela função.
- Repor o contexto: Antes da função terminar.
# Registos
Para evitar a salvaguarda de registos desnecessários, e simplificar o código, o compilador pode fazer convenções específicas.
◉ x0 Constante = 0
◉ x1 Guarda o retorno da função.
◉ x2/sp Ultima posição do stack ocupada
◉ x3/x4 Usados pra facilitar o enderençamento. Podem usar livremente ou como registo temporário para enderençamento.
◉ Temporários Após uma chamada da função M main a função f, podem ter sido modificadas. Assim, se a função M necessita destes registos, têm de ser salvaguardados por esta antes de chamar f (e reposts após a chamada de f).
◉ Saved Registos que contêm variáveis da função M. Têm de ser preservados após uma chamada à função f. A salvaguarda é da responsabilidade da função f.
# Codificação das instruções
Em geral as instruções são codificadas numa palavra de instrução, correspondente à seguinte sequência de campos (fields):
- Opcode: Código da operação a realizar
- Operandos: Identificação (número) dos registos fonte (source) e destino (destination) e/ou valor dos operandos imediatos (constantes codificadas imediatamente na palavra de instrução).
- Outros: Codifica outras opções das instruções.
Dependendo da ISA, o tamanho e ordem de cada um dos campos pode variar. No RISC-V a palavra de instrução tem tamanho fixo (4B).
Por exemplo
Instrução: addi x12,x10,4
Type I = Imm[11:0] ra funct3 rd opcode
Imm[11:0] = 4 = 0000 0000 0100
ra = x10 = 01010
rd = x12 = 01100
funct3 = 000
Opcode = 0010011
Código máquina = 0000 0000 0100 01010 000 01100 0010011 = 0000 0000 0100 0101 0000 0110 0001 0011 = 00450613