Последнее время я очень интересовался компиляторами, низкоуровневыми языками программирования и архитектурой компьютера. Однако на хабре про компиляторы низкоуровневых языков, нашел очень мало информации. Поэтому я решил рассказать о ассемблере и написать свой. Весь код в статье написан на языке Си из-за быстроты программ, написанных на нем, и его простоты.

Набор команд и грамматика
Набор команд не должен быть большим. У меня получилось всего 18 команд.
Теперь, давайте определимся что наш виртуальный процессор должен уметь делать.
Прежде всего, это пересылка данных, работа со стеком и базовая арифметика. Но как же процессор будет давать/получать данные о своей работе?
Для это нам нужно будет сделать ввод-вывод и возвращение результата выполнения. Все. Никаких функций, переменных и переходов. Только хардкор. Однако язык будет условным, как ARM condition language. Раз он условный, нужно вырабатывать условия, пусть это будут делать сравнения.

Теперь простейший пример программы на языке:
al mov r00, #0

al cmp r00, #400

av return #0

le mul r01, r00, r00
le swi #5
le inc r00
le mov r14, #1 


Разбор кода:
al mov r00, #0 — в любом случае r00 = 0
al cmp r00, #400 — в любом случае сравним r00 и 400
av return 0 — если r00 больше, завершаем выполнение с кодом 0
le mul r01, r00, r00 — если r00 меньше или равно, r01 = r00 * r00
le swi #5 — если r00 меньше или равно, закрашиваем пиксель(r00, r01)
le inc r00 — если r00 меньше или равно, увеличим r00 на 1.
le mov r14, #1 — если r00 меньше или равно, счетчик программы = 1

Для начала напишем парсер, создающий из входной строки объектное(общее) представление кода. Самый простой способ это сделать — сохранить условие, команду и аргументы в структуру.

Заголовочный файл:
#ifndef PARSER_H
#define PARSER_H

struct operation {
	char cond[32]; // Условие выполнения
	char cmd[32];  // Сама команда
	char arg0[32]; // Первый аргумент
	char arg1[32]; // Второй аргумент
	char arg2[32]; // Третий агрумент
};

void dump_operation(struct operation *op); // Вывод данных операции
struct operation *parse_code(const char *code); // Парсит строку в общее представление кода

#endif // PARSER_H


Исходный файл:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#include "parser.h"

#define SON(x) (((x) && (*(x) != '\0'))?(x):"NONE") //Если строка пуста или указатель нулевой,  то выдаем NONE (для dump_operation)

void dump_operation(struct operation *op)
{
	if (!op) { // Если дан неверный указатель
		printf("OP: NULL\n");
		return;
	}

        // Если указатель в порядке
	printf("COND: \"%s\"\nOP: \"%s\"\nARGS: \"%s\", \"%s\", \"%s\"\n\n",
               SON(op->cond), SON(op->cmd), SON(op->arg0),
               SON(op->arg1), SON(op->arg2));
}

static inline const char *skip(const char *line, char *skips) // Пропуск строки в строке
{
	line += strlen(skips); // Увеличим указатель на пропарсиваемую строку на длину пропускаемой
	while ((*line == ' ') || (*line == ',')) // Пропустим ненужные символы
		line++;

	if ((*line == '\0') || (*line == '\n') || (*line == ';')) // Если конец строки 
		return 0; // Вернем нулевой указатель

	return line; // Иначе вернем полученный указатель
}

static inline void str_to_lower(char *str) // Переводит строку в нижний регистр
{
	unsigned int l = strlen(str);
	unsigned int i = 0;

	for (i = 0; i < l; i++)
		str[i] = (char)tolower(str[i]);
}

struct operation *parse_code(const char *code)
{
	struct operation *op = (struct operation*)calloc(1, sizeof(*op)); // Выделим память под результат
	char *tokens[5] = {op->cond, op->cmd, op->arg0, op->arg1, op->arg2};
	unsigned int i = 0;

	for (i = 0; i < 5; i++) {
        	if (sscanf(code, "%[0-9a-zA-Z@#$]", tokens[i]) <= 0/*Получаем строку в текущий токен*/) { // Если встретили запрещенный символ
			fprintf(stderr, "Error!\nUnknown symbol!\n"); // Говорим об этом

			free((void *)op); // Освобождаем память
			return 0; // Возвращаем нулевой указатель
        	}

		str_to_lower(tokens[i]); // Переводим строку в нижний регистр

		code = skip(code, tokens[i]); // Пропускаем полученный токен

		if (!code) // Если код закончился
            		break; // Завершаем цикл
	}

	return op; // Возвращаем объектное представление кода
}


В следующей части расскажу об анализе кода и переводе его в байт-код.

Литература по теме
Э. Таненбаум Т.Остин Архитектура компьютера
А. Ахо, Р. Сети, Д. Ульман Компиляторы: принципы, технологии и инструменты

P.S народ, комментируйте, что не нравится. Постараюсь исправить. Заранее спасибо!

Комментарии (4)


  1. StrangerInRed
    11.12.2015 15:28

    Совет: Не используейте массивы фиксированной длинны для ввода и хранения данных, лучше указатели.

    struct operation {
    	char cond[32]; // Условие выполнения
    	char cmd[32];  // Сама команда
    	char arg0[32]; // Первый аргумент
    	char arg1[32]; // Второй аргумент
    	char arg2[32]; // Третий агрумент
    };
    

    по 32 байта на команду и условие. так и память закончится. Для байткодов заведите энум.


    1. aandrew002
      11.12.2015 15:30
      -2

      Да спасибо, сейчас быстро указатели заюзаю, для байткодов энум… У меня по другому, давайте исходник кину…


    1. aandrew002
      11.12.2015 15:48
      -1

      хотя у меня же из файла читается строка, генерируется struct operation, потом дамп в файл, память освоюождается. Т.е используется 160 байт всегда


  1. michael_vostrikov
    11.12.2015 16:37
    +2

    народ, комментируйте, что не нравится. Постараюсь исправить.
    Не нравится статья. Лучше уберите в черновики, серьезно. В ней нет ничего, что могло бы представлять интерес для других.

    Однако на хабре про компиляторы низкоуровневых языков, нашел очень мало информации.
    Потому что низкоуровневый язык как правило один — ассемблер. По слову «ассемблер» находится много статей. А принципы компиляции мало отличаются от высокоуровневых, и компиляторы практически для любой архитектуры процессора уже есть.

    В следующей части расскажу об анализе кода и переводе его в байт-код.
    Если убрать весь код под спойлер, останется около 30 строк текста. Как-то маловато для статьи.

    Никого не хотел обидеть, просьба рассматривать написанное выше как советы и как объяснение, почему ставят минусы. Это хорошо, что вы чем-то интересуетесь и понимаете, как оно работает. Но если вам нечего написать — то и не пишите.