Иногда в жизни случаются моменты, когда в голову приходит новая архитектура процессора, и так не терпится ее проверить, насколько легко писать код, насколько компактным и быстрым он получается. И тогда большинство хватается за первый попавшийся любимый язык, и пишет на нем детский ассемблер для своего нового набора инструкций. А потом останавливается.
Почему? Потому что хочется если уж ассемблер, то макросы. А лучше над ассемблером сразу C или еще что покруче. А ассемблер у нас детский, и крэшится при каждой строчке с неправильным синтаксисом. А хочется настоящего.
Portable Assembler
По-моему это хорошая затея. Представьте — в ассемблер входит парсер, макропроцессор, работает сохранение кода в разные форматы (ELF, a.out, intel hex, …). А набор инструкций и их обработка вынесены в отдельные модули. Тогда для портирования под свою архитектуру достаточно всего лишь переписать один модуль (backend).
Когда я задался этой идеей, я смог найти только один portable assembler — vasm.
Итак, ставим задачу. Есть такая платформа — CHIP8. Славится она тем, что она очень простая. В ассемблере всего 35 инструкций. Но это значит, что закодить их у нас не займет много времени.
Для начала, установим vasm. Качаем http://sun.hasenbraten.de/vasm/release/vasm.tar.gz
Допустим наш модуль будет называться c8
. Синтаксис будет стандартный (как для x86, intel syntax).
$ make CPU=c8 SYNTAX=std
Поскольку никакого модуля c8 еще нет, то сборка не проходит. Создадим необходимые файлы:
$ mkdir cpus/c8 # папка с проектом $ touch cpus/c8/cpu.h # определения нашего процессора $ touch cpus/c8/cpu.c # код генератора $ cpus/c8/cpu_errors.h # список сообщений об ошибках при компиляции c8-ассемблера
Теперь необходимо реализовать несколько обязательных функций и переменных. Сделаем пока заглушки.
В Makefile вверху добавим (чтобы не указывать каждый раз вручную):
CPU = c8 SYNTAX = std
В cpu.h объявим:
#define VASM_CPU_C8 1 /* Идентификатор нашего CPU */ #define LITTLEENDIAN 1 /* CHIP8 - little endian */ #define BIGENDIAN 0 #define MAX_OPERANDS 3 /* Инструкция принимает до 3 команд */ #define MAX_QUALIFIERS 0 /* без квалификаторов (типа mov.l, mov.s...) */ typedef int16_t taddr; /* Адресное пространство - 16-битное */ #define INST_ALIGN 2 /* Выравнивание по 2 байта (не обязательно) */ #define DATA_ALIGN(n) 1 /* Для данных выравнивания нет */ /* Структура операнда (аргумента инструкции) */ typedef struct { int type; /* Тип инструкции (см. ниже OP_VREG, OP_IREG...) */ int reg; /* Номер регистра V0=0, V1=1, ... */ expr *value; /* Выражение (для IMM-операндов) */ } operand; /* Типы операндов */ #define OP_VREG 1 #define OP_IREG 3 #define OP_IMM 4 #define OP_KREG 5 #define OP_DTREG 6 #define OP_STREG 7 #define OP_FREG 8 #define OP_HFREG 9 #define OP_IND 10 /* Форматы инструкций */ #define OP_nnnn 1 #define OP_nNNN 2 #define OP_nnnN 3 #define OP_nXYn 4 #define OP_nXNN 5 #define OP_nXYN 6 #define OP_nXnn 7 #define DATA_OPERAND(n) OP_IMM /* Данные - это тип OP_IMM */ /* Инструкция - маска (opcode) и формат (optype) */ typedef struct { int opcode; int optype; } mnemonic_extension;
И наконец-то: cpu.c
#include "vasm.h" char *cpu_copyright = "vasm CHIP8 cpu backend"; char *cpuname = "c8"; int bitsperbyte = 8; int bytespertaddr = 4; #define ADDRESS_START 0x200 #define ADDRESS_END 0xfff mnemonic mnemonics[] = { /* TODO */ }; int mnemonic_cnt = sizeof(mnemonics)/sizeof(mnemonics[0]); /* Эти функции для над пока не важны */ int init_cpu() { return 1; /* Successful initialization */ } int cpu_args(char *args) { return 0; /* No args */ } char *parse_cpu_special(char *s) { return s; /* No specials */ } operand *new_operand() { operand *op = malloc(sizeof(*op)); return op; } taddr instruction_size(instruction *p, section *sec, taddr pc) { return 2; /* все инструкции по 2 байта */ } /* А вот эти надо будет подправить */ /* Парсер одного операнда - заполняет поля переменной op */ int parse_operand(char *text, int len, operand *op, int requires) { } /* Обработка одной инструкции, возвращает байт-код, который ей соответствует */ dblock *eval_instruction(instruction *p, section *sec, taddr pc) { } /* Обработка данных */ dblock *eval_data(operand *op, taddr bitsize, section *sec, taddr pc) { }
Да-да, всего 3 функции — и мы у цели.
Дописываем
Для начала формируем набор мнемоник. Там формат простой — строка, определяющая инструкцию, набор операндов, и экстра-флаги (мы эту структуру экстра-флагов сами в cpu.h объявляли).
Вот что у меня вышло для CHIP-8:
mnemonic mnemonics[] = { "high", {}, {0x00ff, OP_nnnn}, /* Set SCHIP graphics mode */ "low", {}, {0x00fe, OP_nnnn}, /* set CHIP graphics mode */ "cls", {}, {0x00e0, OP_nnnn}, /* clear screen */ "exit", {}, {0x00fd, OP_nnnn}, /* exit from the programm */ "scll", {}, {0x00fc, OP_nnnn}, /* scroll left */ "sclr", {}, {0x00fb, OP_nnnn}, /* scroll right */ "scld", {OP_IMM}, {0x00c0, OP_nnnN}, /* scroll screen down */ "call", {OP_IMM}, {0x2000, OP_nNNN}, /* call */ "jp", {OP_IMM}, {0x1000, OP_nNNN}, /* jump */ "jp", {OP_IMM, OP_VREG}, {0xb000, OP_nNNN}, /* jump to addr + v0 */ "ret", {}, {0x00ee, OP_nnnn}, /* return from subroutine */ "drw", {OP_VREG, OP_VREG, OP_IMM}, {0xd000, OP_nXYN}, /* draw */ "se", {OP_VREG, OP_IMM}, {0x3000, OP_nXNN}, /* skip if vx == NN */ "se", {OP_VREG, OP_VREG}, {0x5000, OP_nXYn}, /* skip if vx == vy */ "sne", {OP_VREG, OP_IMM}, {0x4000, OP_nXNN}, /* skip if vx != NN */ "sne", {OP_VREG, OP_VREG}, {0x9000, OP_nXYn}, /* skip if vx != vy */ "skp", {OP_VREG}, {0xe09e, OP_nXnn}, /* skip if key in vx pressed */ "sknp", {OP_VREG}, {0xe0a1, OP_nXnn}, /* skip if key in vx not pressed */ "rnd", {OP_VREG, OP_IMM}, {0xc000, OP_nXNN}, "add", {OP_VREG, OP_IMM}, {0x7000, OP_nXNN}, "add", {OP_VREG, OP_VREG}, {0x8004, OP_nXYn}, "add", {OP_VREG, OP_IREG}, {0xf01e, OP_nXnn}, /* I = I + vx */ "sub", {OP_VREG, OP_VREG}, {0x8005, OP_nXYn}, "subn",{OP_VREG, OP_VREG}, {0x8007, OP_nXYn}, "or", {OP_VREG, OP_VREG}, {0x8001, OP_nXYn}, "and", {OP_VREG, OP_VREG}, {0x8002, OP_nXYn}, "xor", {OP_VREG, OP_VREG}, {0x8003, OP_nXYn}, "shl", {OP_VREG}, {0x800e, OP_nXnn}, /* vx >> 1 */ "shr", {OP_VREG}, {0x8006, OP_nXnn}, /* vx >> 1 */ "bcd", {OP_VREG}, {0xf033, OP_nXnn}, /* Store BCD representation at I to vr */ /* Store registers V0 to [register] from HP Flags where [register] < 8 */ "hps", {OP_VREG}, {0xf075, OP_nXnn}, /* Load registers V0 to [register] from HP Flags where [register] < 8 */ "hpl", {OP_VREG}, {0xf085, OP_nXnn}, "ld", {OP_VREG, OP_IMM}, {0x6000, OP_nXNN}, /* Move NN to vx */ "ld", {OP_VREG, OP_VREG}, {0x8000, OP_nXYn}, /* Move vx to vy */ "ld", {OP_IREG, OP_IMM}, {0xa000, OP_nNNN}, /* Point I to NNN */ "ld", {OP_FREG, OP_VREG}, {0xf029, OP_nXnn}, /* Point I to lores char in vr */ "ld", {OP_HFREG, OP_VREG}, {0xf030, OP_nXnn}, /* Point I to hires char in vr */ "ld", {OP_IND, OP_VREG}, {0xf055, OP_nXnn}, /* Store v0..vr to memory at I */ "ld", {OP_VREG, OP_IND}, {0xf065, OP_nXnn}, /* Load v0..vr from memory at I */ "ld", {OP_VREG, OP_DTREG}, {0xf007, OP_nXnn}, /* Store DT into vr */ "ld", {OP_DTREG, OP_VREG}, {0xf015, OP_nXnn}, /* Store vr into DT */ "ld", {OP_VREG, OP_STREG}, {0xf018, OP_nXnn}, /* Store vr into ST */ "ld", {OP_VREG, OP_KREG}, {0xf00a, OP_nXnn} /* Wait and store key into vr */ };
Тут все делалось по бумажкам — по документации с набором инструкций. Думаю, должно быть понятно, но если что — пишите в комментариях, разъясню подробнее.
В функции parse_operand() надо смотреть на строку, и определять тип операнда. Это могут быть регистры (v0, v1, …vf, i, k, st, dt, lf, hf) или косвенная адресация «[i]», или просто численное выражение. Проверяем все типы:
int parse_operand(char *text, int len, operand *op, int requires) { const char hex[] = "0123456789abcdef"; char *p; p = skip(text); p[0] = tolower((unsigned char) p[0]); if (len > 1) { p[1] = tolower((unsigned char) p[1]); } if (len == 2 && p[0] == 'v' && strchr(hex, p[1]) != 0) { op->type = OP_VREG; op->reg = strchr(hex, p[1]) - hex; } else if (strncmp(p, "i", len) == 0) { op->type = OP_IREG; } else if (strncmp(p, "k", len) == 0) { op->type = OP_KREG; } else if (strncmp(p, "dt", len) == 0) { op->type = OP_DTREG; } else if (strncmp(p, "st", len) == 0) { op->type = OP_STREG; } else if (strncmp(p, "f", len) == 0) { op->type = OP_FREG; } else if (strncmp(p, "hf", len) == 0) { op->type = OP_HFREG; } else if (strncmp(p, "[i]", len) == 0) { op->type = OP_IND; } else { op->type = OP_IMM; op->value = parse_expr(&p); } return (requires == op->type); }
Теперь когда инструкция разобрана по частям, известны все типа операндов и их значения — можно формировать байт-код этой инструкции. Для этого будет вызвана наша функция eval_instruction():
dblock *eval_instruction(instruction *p, section *sec, taddr pc) { int i; dblock *db = new_dblock(); mnemonic *m; uint16_t opcode; taddr values[MAX_OPERANDS] = {0,0,0}; m = &mnemonics[p->code]; opcode = m->ext.opcode; db->size = instruction_size(p, sec, pc); db->data = malloc(db->size); /* вычисляем IMM-операнды */ for (i = 0; i < MAX_OPERANDS; i++) { operand *op = p->op[i]; if (op != NULL) { if (op->value != NULL && op->type == OP_IMM) { if (!eval_expr(op->value, &values[i],sec,pc)) { symbol *base; if (base = find_base(op->value,sec,pc)) { int type=REL_ABS,offs=8,size=16; rlist *rl; rl = add_reloc(&db->relocs,base,values[i],type,size,offs); } } } else if (op->type == OP_VREG) { values[i] = op->reg; } } } /* Формируем байт-код по маске и операндам */ switch (m->ext.optype) { case OP_nnnn: break; /* do nothing */ case OP_nnnN: opcode |= (values[0] & 0xf); break; case OP_nNNN: if (p->op[0]->type == OP_IMM) // jp NNN opcode |= (values[0] & 0xfff); else // ld I, NNN opcode |= (values[1] & 0xfff); break; case OP_nXNN: opcode |= ((values[0] & 0xf) << 8)|(values[1] & 0xff); break; case OP_nXYN: opcode |= ((values[0] & 0xf) << 8) | ((values[1] & 0xf) << 4) | (values[2] & 0xf); break; case OP_nXYn: opcode |= ((values[0] & 0xf) << 8) | ((values[1] & 0xf) << 4); break; case OP_nXnn: opcode |= ((values[0] & 0xf) << 8); break; } db->data[0] = opcode >> 8; db->data[1] = opcode & 0xff; return db; }
Все. Теперь мы можем собрать нашу первую программу. Пишем файл:
cls ld v0, 5 ld v1, 0xf0 add v0, v1
Ассемблируем и смотрим на результат:
$ ./vasmc8_std test.c8 -Fbin -o test.bin $ hexdump test.bin e000 0560 f061 1480 Фактически это 4 инструкции: 000e # cls 6005 # ld v0, 00 61f0 # ld v1, f0 8014 # add v0, v1
Значит все работает. Осталось доделать обработку сегмента данных:
dblock *eval_data(operand *op, taddr bitsize, section *sec, taddr pc) { dblock *new=new_dblock(); taddr val; new->size=(bitsize+7)/8; new->data=mymalloc(new->size); if (!eval_expr(op->value,&val,sec,pc)&&bitsize!=32) cpu_error(0); /* Это будет наше 1-е сообщение об ошибке в cpu_errors.h */ if (bitsize==8) { new->data[0]=val; } else if(bitsize==16) { new->data[0]=val>>8; new->data[1]=val; } return new; }
Этот код я взял из других ассемблеров (в состав VASM входят ассемблеры 6502, arm, m68k, x86, c16x, z80, ppc и игрушечный test). Здесь просто парсится выражение, и результат сохраняется как 2 байта в новый блок байт-кодов.
Теперь мы можем скомпилировать любой код для CHIP-8. Например, этот:
.org 0x200 ld v0, 0 ld v1, 0 loop: ld I, left rnd v2, 1 se v2, 1 ld I, right drw v0, v1, 4 add v0, 4 se v0, 64 jp loop ld v0, 0 add v1, 4 se v1, 32 jp loop fin: jp fin right: .byte 0b10000000 .byte 0b01000000 .byte 0b00100000 .byte 0b00010000 left: .byte 0b00100000 .byte 0b01000000 .byte 0b10000000 .byte 0b00010000
Если его собрать и запустить в любом из эмуляторов, то получим лабиринт.
Выводы
Вы не забыли что это мы делали, чтобы научиться портировать ассемблеры на другие платформы?
Так вот, мое мнение — VASM это ужасный продукт с жутким кодом. Начнете его читать — поймете. Хочется чего-то более красивого и высокоуровневого. Можно на том же Go с его красивыми «interface» и объектами. Но для начала сойдет и так.
К слову о CHIP-8 я когда-то делал эмулятор — chipr. Он так и остался непонятым, но у меня все работает и запускаются эти чудные черно-белые игры в 200 байт кода.
Ну и продолжая тему portable compilers — есть язык такой — smallC. Это может стать следующим шагом для создания языков программирования под ваши вымышленные архитектуры.
Изобретайте, проверяйте, улучшайте!
Спасибо за статью, интересная тема.
Я тоже как-то писал эмулятор Chip8 http://code.google.com/p/emuchip/
А сейчас думаю написать эмулятор чего-нибудь типа GameBoy или MasterSystem на Go.