Você já teve a pergunta "como o processador funciona?". Sim, sim, exatamente o que está no seu PC / laptop / smartphone. Neste artigo, quero dar um exemplo de um processador auto-inventado com um design no Verilog. Verilog não é exatamente a linguagem de programação que parece. Este é o idioma da descrição do hardware. O código escrito não é executado por nada (a menos que você o execute no simulador, é claro), mas se transforma no design do circuito físico ou na forma percebida pelo FPGA (Field Programmable Gate Array).
Isenção de responsabilidade: este artigo é o resultado do trabalho em um projeto na universidade, portanto o tempo para o trabalho foi limitado e muitas partes do projeto ainda estão apenas no estágio inicial de desenvolvimento.
Observe que o processador criado neste artigo tem pouco em comum com os modernos processadores comuns, mas tentei alcançar um objetivo um pouco diferente com sua criação.
Para entender realmente o processo de programação, você precisa imaginar como cada uma das ferramentas utilizadas funciona: um compilador / intérprete da linguagem, uma máquina virtual, se houver, código intermediário e, é claro, o próprio processador. Muitas vezes, as pessoas que estudam programação estão no primeiro estágio por um longo tempo - apenas pensam em como a linguagem e seu compilador funcionam. Isso geralmente leva a erros cujas soluções são desconhecidas para o programador iniciante, porque ele não tem idéia de onde estão vindo as raízes desses problemas. Eu mesmo vi vários exemplos ao vivo em que a situação era semelhante à descrição acima, então decidi tentar corrigi-la e criar um conjunto de coisas que ajudarão os iniciantes a entender todas as etapas.
Este kit consiste em:
- Linguagem realmente inventada
- Destaque o plugin para o VS Code
- Compilador para ele
- Conjunto de instruções
- Um processador simples capaz de executar este conjunto de instruções (escrito em Verilog)
Recordo mais uma vez que este artigo não descreve nada semelhante a um processador real moderno, descreve um modelo fácil de entender sem entrar em detalhes.
O que você precisa para fazer você mesmo:
Para executar uma simulação de CPU, você precisa do ModelSim, que pode ser baixado do site da Intel.
Para executar o compilador OurLang, você precisa da versão Java> = 8.
Links para projetos:
https://github.com/IamMaxim/OurCPU
https://github.com/IamMaxim/OurLang
Extensão:
https://github.com/IamMaxim/ourlang-vscode
Para criar a parte do Verilog, eu geralmente uso um script bash:
#/bin/bash vlib work vlog *.v vsim -c testbench_1 -do "run; exit"
Mas isso pode ser repetido através da GUI.
É conveniente usar o Intellij IDEA para trabalhar com o compilador. O principal é acompanhar quais módulos o módulo que você precisa nas dependências possui. Não publiquei o .jar pronto para abrir o acesso, porque espero que o leitor leia o código-fonte do compilador.
Os módulos lançados são Compilador e Intérprete. Tudo está claro com o compilador, o Interpreter é apenas um simulador do OurCPU em Java, mas não o consideraremos neste artigo.
Conjunto de instruções
Eu acho que é melhor começar com o conjunto de instruções.
Existem várias arquiteturas de conjuntos de instruções:
- Baseado em pilha é o que é descrito no artigo. Uma característica distintiva é que todos os operandos são empurrados para a pilha e removidos da pilha, o que elimina imediatamente a possibilidade de execução paralelizada, mas é uma das abordagens mais simples para trabalhar com dados.
- Baseado no acumulador - o resultado final é que existe apenas um registro que armazena um valor que é modificado por instruções.
- Baseado em registro é o que é usado nos processadores modernos, porque permite alcançar o desempenho máximo através do uso de várias otimizações, incluindo paralelização de execução, pipelining, etc.
Nosso conjunto de instruções do processador contém 30 instruções
Em seguida, proponho uma olhada na implementação do processador:
O código consiste em vários módulos:
- CPU
- RAM
- Módulos para cada instrução
A RAM é um módulo que contém diretamente a própria memória, bem como uma maneira de acessar dados nela.
CPU - um módulo que controla diretamente o andamento do programa: lê instruções, transfere o controle para a instrução desejada, armazena os registros necessários (ponteiro para a instrução atual, etc.).
Quase todas as instruções funcionam apenas com a pilha, então siga-as. Alguns (por exemplo, putw, putb, jmp e jif) têm um argumento adicional na própria instrução. Eles precisam passar a instrução inteira para que possam ler os dados necessários.
Aqui está um resumo geral de como o processador funciona:

Princípios gerais de design de dispositivos no nível de instrução
Acho que é hora de se familiarizar com o dispositivo diretamente dos próprios programas. Como você pode ver no diagrama acima, após cada instrução, o endereço passa para o próximo. Isso fornece um curso linear para o programa. Quando se torna necessário quebrar essa linearidade (condição, loop, etc.), são utilizadas instruções de ramificação (em nosso conjunto de instruções são jmp e jif).
Ao chamar funções, precisamos salvar o estado atual de tudo e, para isso, existem registros de ativação - registros que armazenam essas informações. Eles não estão vinculados ao processador ou às instruções de forma alguma, é apenas um conceito que o compilador usa ao gerar código. O registro de ativação em OurLang tem a seguinte estrutura:

Como pode ser visto neste diagrama, as variáveis locais também são armazenadas no registro de ativação, o que permite calcular o endereço da variável na memória em tempo de compilação, em vez de em tempo de execução, agilizando a execução do programa.
Para chamadas de função, nosso conjunto de instruções fornece métodos para trabalhar com dois registradores contidos no módulo da CPU (ponteiro de operação e ponteiro de endereço de ativação) - putopa / popopa, putara / popara.
Compilador
Agora vamos dar uma olhada na parte mais próxima do programador final - o compilador. Em geral, o compilador como um programa consiste em 3 partes:
- Lexer
- Analisador
- Compilador
O lexer é responsável por traduzir o código fonte do programa em unidades lexicais que o analisador entende.
O analisador cria uma árvore de sintaxe abstrata a partir dessas unidades lexicais.
O compilador passa por essa árvore e gera algum tipo de código que consiste em instruções de baixo nível. Pode ser um bytecode ou um código binário pronto para ser executado pelo processador.
No compilador OurLang, essas partes são representadas respectivamente por classes
- Lexer.java
- Parser.java
- Compiler.java
Linguagem
O OurLang está em sua infância, ou seja, funciona, mas até agora não há muitas coisas nele e mesmo a parte principal do idioma não foi finalizada. Mas, para entender a essência do compilador, o estado atual é suficiente.
Como exemplo de um programa para entender a sintaxe, este fragmento de código é proposto (também é usado para testar a funcionalidade):
// single-line comments /* * Multi-line comments */ function print(int arg) { instr(putara, 0); instr(putw, 4); instr(add, 0); instr(lw, 0); instr(printword, 0); } function func1(int arg1, int arg2): int { print(arg1); print(arg2); if (arg1 == 0) { return arg2; } else { return func1(arg1 - 1, arg2); }; } function main() { var i: int; i = func1(1, 10); if (i == 0) { i = 1; } else { i = 2; }; print(i); }
Não vou me concentrar no idioma, deixarei para o seu estudo. Através do código do compilador, é claro;).
Ao escrevê-lo, tentei tornar o código autoexplicativo que fica claro sem comentários, para que não haja problemas em entender o código do compilador.
Bem, é claro, a coisa mais interessante é escrever código e assistir ao que ele se transforma. Felizmente, o compilador OurLang gera código semelhante a assembly com comentários,
o que ajuda a não ficar confuso sobre o que está acontecendo lá dentro.
Também recomendo instalar a extensão do Visual Studio Code, pois facilitará o trabalho com o idioma.
Boa sorte para aprender o projeto!