ForgeZero: Как я перестал бояться линкеров и написал универсальный билдер для ассемблера на NodeJs(после Go)
Всем привет, низкоуровневые программисты и сочувствующие.
Месяца два назад я ковырял операционную систему в реальном режиме с поддержкой 64-бит. Работа с ассемблером была ежедневной рутиной: криптографические протоколы, системные вызовы, ручное управление памятью. И мне надоело. Надоело бесконечно вбивать цепочки команд nasm …, ld …, следить за флагами и разгребать мусор из объектных файлов.
Я решил написать свой билдер. Сначала на Node.js (просто для скорости прототипирования), а сейчас я в процессе полного переезда на Go. И нет, не только потому что это модно — просто когда ты пишешь системный инструмент, он должен быть быстрым, нативным и не требовать рантайма в 100 мегабайт.
Вы пишете код на NASM, GAS или FASM, а потом начинаются танцы с бубном: какой флаг передать, как слинковать, почему на macOS один линкер, на Linux — другой, а на Windows — третий, и где взять отладочные символы, когда программа падает в segmentation fault. Я создал билдер, который кладёт конец этому хаосу. И сейчас расскажу, почему он реально крут, без прикрас.
Что он вообще делает?
Вы пишете: node index.js main.asm И получаете исполняемый файл. Всё, билдер сам:
Находит все .asm (или .s, .fasm) в папке (рекурсивно!). Вызывает нужный ассемблер с правильными флагами. Выбирает правильный линкер для вашей ОС (ld для Linux, gcc для Windows, ld для macOS). Линкует с библиотеками, если надо. Опционально чистит объектники.
Почему он уникален?
- Поддержка ТРЁХ ассемблеров в одном флаконе
Большинство билдеров заточены под один ассемблер. Например, make с правилами для NASM — всё. А тут:
NASM (стандарт для x86)
node index.js program.asm
GAS (GNU Assembler, синтаксис AT&T)
node index.js --assembler gas program.s
FASM
node index.js --assembler fasm program.fasm
Никакой смены инструментов — один билдер правит всеми. Для меня это был вызов: у каждого ассемблера свои флаги (NASM требует -f elf64, GAS — просто as, FASM — format ELF64 в коде). Но я сделал так, что вы об этом забываете. Флаг --format работает для NASM, а для FASM он… ну, почти работает, но вы поняли.
I. Кроссплатформенная линковка без головной боли
Вы знаете, что на Linux системный линкер — ld, на Windows — gcc (через MinGW), на macOS — тоже ld, но с другими флагами? А я знаю. Билдер определяет вашу платформу и:
На Linux добавляет -dynamic-linker /lib64/ld-linux-x86-64.so.2, На Windows вызывает gcc (потому что ld там не умеет в форматы PE), На macOS — ld с родными настройками.
И вы просто получаете ./program, который работает. Без #ifdef в голове.II. Отладка — не роскошь, а норма
Вы когда-нибудь пытались отлаживать ассемблерную программу без символов? Я да. Это боль. Поэтому в билдере есть флаг --debug:
node index.js --debug factorial.asm
gdb ./factorial
(gdb) break main
(gdb) run
Он добавляет -g для ассемблера (где это возможно) и для линкера. Для FASM, правда, пришлось извращаться (-d DEBUG=1), но это работает. Теперь вы можете нормально ставить брейкпоинты и смотреть регистры.
Что ещё делает его крутым (и честно, почему я горжусь)
Я разделил код на args.js, assembler.js, linker.js, builder.js, logger.js. Это не ради красивых слов — это значит, что вы можете взять только линковщик или только парсер аргументов в свой проект. Или добавить свой ассемблер (YASM? FASM? LLVM? — дайте знать, я добавлю).
Защита от идиотских ошибок
Однажды я случайно перезаписал исходный код скомпилированным бинарником, потому что билдер назвал выходной файл так же, как входной. С тех пор в коде есть проверка: если output == input — он либо меняет имя, либо выдаёт ошибку. Больше никакого rm -rf судьбы.
А в чём подвох? (честная правда)
Билдер не идеален. Вот что он пока не умеет:
Параллельная компиляция — пока собирает файлы по одному. На сотне файлов будет медленно. Не поддерживает Windows без MinGW — если у вас голый Windows без gcc, линковка упадёт. FASM всё ещё капризничает — с флагом --debug иногда ругается на illegal instruction (но я чиню). Нет конфигурационного файла — все флаги только из командной строки. Хотя это и плюс для простоты.
Но для 99% задач — собрать один-два ассемблерных файла с libc или без — он работает как часы.
Почему я написал этот билдер?
Я устал от фрагментации. Ассемблер — это низкий уровень, так почему инструменты для него должны быть сложными? Я хотел, чтобы взять и запустить — как gcc main.c. Теперь у меня (и у вас) это есть.
Попробуйте. Если найдёте баг — пожалуйста, скажите. Я его пофикшу.
Прямо сейчас я веду разработку новой версии на Go в отдельной ветке. Основной движок переписан, парсер аргументов и вызов ассемблеров работают. FASM с --debug тоже чинится (больше никаких illegal instruction — я наконец-то понял, что FASM не любит -d` без значения).
Когда Go-версия догонит Node.js по функциональности (а это случится в ближайшие недели), я сделаю релиз 2.0. Node.js версия останется в архиве как исторический прототип, но основной билдер будет на Go.
Репо - https://github.com/alexvoste/ForgeZero