Escrevendo um processador e ambiente simples para ele

Olá Neste artigo, mostrarei quais etapas você precisa seguir para criar um processador e um ambiente simples para ele.


Arquitetura do Conjunto de Comandos (ISA)


Primeiro você precisa decidir qual será o processador. Os seguintes parâmetros são importantes:


  • O tamanho da palavra e dos registros da máquina (bit / "bit" do processador)
  • Instruções da máquina (instruções) e seu tamanho

As arquiteturas de processador podem ser divididas em 2 tipos, de acordo com o tamanho das instruções (na verdade, existem mais, mas outras opções são menos populares):



A principal diferença é que os processadores RISC têm o mesmo tamanho de instrução. Suas instruções são simples e executam-se relativamente rapidamente, enquanto os processadores CISC podem ter tamanhos de instruções diferentes, alguns dos quais podem demorar um pouco.


Decidi fazer um processador RISC muito parecido com o MIPS .


Fiz isso por vários motivos:


  • É bastante simples criar um protótipo desse processador.
  • Toda a complexidade desse tipo de processador é transferida para programas como assembler e / ou compilador.

Aqui estão os principais recursos do meu processador:


  • Tamanho da palavra e registro da máquina - 32 bits
  • 64 registros (incluindo contador de comandos )
  • 2 tipos de instruções

O tipo de registro (a seguir denominado tipo de registro) fica assim:


rtype


A peculiaridade de tais instruções é que elas operam com três registros.


Tipo imediato :


itype


Instruções deste tipo operam com dois registros e um número.


OP é o número da instrução a ser executada (ou para indicar que esta instrução do tipo Registro ).


R0 , R1 , R2 são números de registro que servem como operandos para a instrução.


Func é um campo adicional usado para indicar o tipo de instruções do tipo Registro .


Imm é o campo em que o valor é gravado, o qual queremos fornecer explicitamente instruções como um operando.


  • Apenas 28 instruções

Uma lista completa de instruções pode ser encontrada no repositório do github .


Aqui estão apenas alguns deles:


nor r0, r1, r2 

NOR é uma instrução do tipo Register que executa um OR ORY lógico nos registradores r1 e r2, após o qual grava o resultado no registrador r0.


Para usar esta instrução, é necessário alterar o campo OP para 0000 e o campo Func para 0000000111 no sistema de números binários.


 lw r0, n(r1) 

LW é uma instrução do tipo Imediato que carrega um valor de memória em r1 + n no registro r0.


Para usar esta instrução, por sua vez, é necessário alterar o campo OP para 0111 e escrever o número n no campo IMM .


Escrevendo código do processador


Após criar o ISA, você pode começar a escrever o processador.


Para isso, precisamos conhecer algum tipo de linguagem de descrição de equipamento. Aqui estão alguns deles:


  • Verilog
  • VHDL (não confunda com o anterior!)

Eu escolhi a Verilog, porque programar fazia parte do meu curso universitário.


Para escrever um processador, você precisa entender a lógica de sua operação:


  1. Obter instruções no contador de comandos (PC)
  2. Instruções de decodificação
  3. Execução de instruções
  4. Adicionando ao contador o tamanho do comando da instrução executada

E assim por diante ad infinitum.


Acontece que você precisa criar vários módulos:



Analisaremos cada módulo individualmente.


Registrar arquivo


Um arquivo de registro fornece acesso aos registros. Com isso, você precisa obter os valores de alguns registros ou alterá-los.


No meu caso, tenho 64 registros. Em um dos registros, o resultado da operação nos outros dois é gravado; portanto, preciso oferecer a oportunidade de alterar apenas um e obter os valores dos outros dois.


Decodificador


Um decodificador é a unidade responsável pelas instruções de decodificação. Indica quais operações precisam ser executadas pela ALU e outras unidades.


Por exemplo, a instrução addi deve adicionar o valor do registro $ zero (sempre armazena 0 ) e 20 e colocar o resultado no registro $ t0.


 addi $t0, $zero, 20 

Neste ponto, o decodificador determina que esta instrução:


  • Tipo imediato
  • Deve escrever o resultado no registro

E transfere essas informações para os seguintes blocos.


ALU


Depois que o controle passa para a ALU. Geralmente, realiza todas as operações lógicas e matemáticas, bem como operações de comparação de números.


Ou seja, se considerarmos a mesma instrução addi , nesse estágio ocorre a adição de 0 e 20 .


Outros


Além dos blocos acima, o processador deve ser capaz de:


  • Obter e alterar valores na memória
  • Executar saltos condicionais

Aqui e ali, você pode ver como fica o código.


Montador


Depois de escrever o processador, precisamos de um programa que converta comandos de texto em código de máquina para não fazê-lo manualmente. Portanto, você precisa escrever o assembler.


Eu decidi implementá-lo na linguagem de programação C.


Como meu processador possui uma arquitetura RISC , para simplificar minha vida, decidi projetar o assembler para poder adicionar facilmente minhas próprias pseudo instruções (combinações de várias instruções elementares ou outras pseudo instruções).


Você pode implementar isso usando uma estrutura de dados que armazena o tipo de instrução, seu formato, um ponteiro para uma função que retorna códigos de instruções da máquina e seu nome.


Um programa regular começa com uma declaração de segmento.


Dois segmentos .text são suficientes para nós - nos quais o código fonte de nossos programas será armazenado - e .data - nos quais nossos dados e constantes serão armazenados.


Uma instrução pode ser assim:


 .text jie $zero, $zero, $zero #  addi $t1, $zero, 2 # $t1 = $zero + 2 lw $t1, 5($t2) # $t1 = *($t2 + 5) syscall 0, $zero, $zero # syscall(0, 0, 0) la $t1, label# $t1 = label 

Primeiro, o nome da instrução é indicado, depois os operandos.


Em .data , as declarações de dados são indicadas.


 .data .byte 23 #   1  .half 1337 #   2  .word 69000, 25000 #   4  .asciiz "Hello World!" #     ( ) .ascii "12312009" #   ( ) .space 45 #  45  

Um anúncio deve começar com um ponto e um nome de tipo de dados, seguidos por constantes ou argumentos.


É conveniente analisar (varrer) o arquivo assembler da seguinte maneira:


  1. Primeiro, verifique o segmento
  2. Se for um segmento .data , analisamos diferentes tipos de dados ou um segmento .text
  3. Se for um segmento .text , analisamos comandos ou um segmento .data

Para funcionar, o montador precisa passar pelo arquivo de origem 2 vezes. Na primeira vez em que ele considera quais são os desvios dos links (eles servem), eles geralmente ficam assim:


  la $s4, loop #   loop  s4 loop: # ! mul $s2, $s2, $s1 # s2 = s2 * s1 addi $s1, $s1, -1 # s1 = s1 - 1 jil $s3, $s1, $s4 #  s3 < s1     

E na segunda passagem, você já pode gerar um arquivo.


Sumário


No futuro, você pode executar o arquivo de saída do assembler em nosso processador e avaliar o resultado.


Além disso, o montador pronto pode ser usado no compilador C. Mas isso é mais tarde.


Referências:


Source: https://habr.com/ru/post/pt430680/


All Articles