Сегодня мне удалось добиться от cc65 совершенно идеальной работы.
Я получил возможность собирать из исходных текстов сразу FIL-контейнер с файлом,
причем для этого использовались только штатные утилиты cc65: ca65 и ld65.
Но чтобы объяснить механизм такого синтеза, следует рассказать об очень мощной вещи, реализованной в современных ассемблерах, в т.ч. в ca65. Речь пойдет о Сегментах.
Сегмент (иногда ещё называют "Секция") - это некий регион адресного пространства, имеющий собственные аттрибуты: счётчик адреса, особенности размещения в ОЗУ или ПЗУ, размеры (текущие и максимальные) и разное другое.
Сегменты существовали даже в ДОК: если помните команды DSECT / DEND. Они создавали некую обособленную область, которая никуда не записывалась и нужна была только для расчёта смещений меток. Использовалась, как правило, так:
DSECT
ORG $E8
X1 DS 1
X2 DS 4
DEND
При этом метка X1 получала значение $E8, а X2 - $E9. Смысл этого состоял в том, что появлялась возможность автоматически распределять память для переменных в нулевой странице. При объявлении очередного региона DSECT / DEND счётчик начинал считать вновь от нуля (или от ORG, если она присутствовала).
Т.е. работа велась как бы в двух сегментах: в DSECT / DEND и в основном (который записывался в файл и/или копировался в ОЗУ).
Механизм DSECT / DEND использовался, наверное, 2/3 программистов Агата. Остальные даже его игнорировали и писали всё в фикс-стиле:
X1 EQU $E8
X2 EQU $E9 ..$EC
Но реальная большая прога запросто имела 3-4, а то и больше неявных сегментов (т.е. описанных только в комментариях или мозгах автора). Считаем:
1) Переменные в нулевой странице;
2) Переменные в основной памяти (если в нулевой странице места не хватает), массивы, значения которых не важны до запуска программы (т.е. программа заполняет их по мере работы);
3) То же переменные и массивы, но значения которых заданы до начала работы программы;
4) Те же переменные и массивы, но значения которых вообще недопустимо изменять (выводимый на экран текст, например);
5) Код в основной памяти (хотя бы тот, который части программы - то есть те же сегменты - раскидывает по разным регионам ОЗУ);
6) А может быть и код в дополнительной памяти (ЭмПЗУ);
Теперь представим себе, что у нас есть исходники двух библиотек: L1 и L2. Каждая из них использует ячейки нулевой страницы: например, переменные VAR1 и VAR2. Как распределить между ними память? Есть пара путей:
1) Пишем в начале основной программы VAR1 EQU $E0, VAR2 EQU $E1 - т.е. каждой переменной назначем фиксированные адреса переменных.
Недостаток способа в том, что нам нужно вычленить все переменные из библиотеки L1 и L2 и разместить их где-то в начале основной программы. Я сам часто пользовался этим способом. Это не было самой сложной или долгой операцией, но это всё равно требует времени. К тому же нужно хорошо знать библиотеки, знать, какие у них есть требования к размещению переменных. Зато в заголовке программы наглядно видны все используемые рессурсы.
2) Пишем в начале каждой библиотеки всё те же статические EQU и надеемся, что они не пересекутся.
Этот путь применялся практически всеми разработчиками широко используемых библиотек: RWTS, IOSub, ... да тех же Бейсика и ДОС. EQU можно заменить на DSECT / DEND, суть не изменится. Библиотеку можно будет подключать к основной программе на уровне исходника или бинаря. Ну, правда, есть проблема: если подключить как исходник, то получается нечто вроде:
LDA VAR1
CHN L1
VAR1 EQU $28
LDA (VAR1),Y
Синтаксически почти всё верно, но ассемблер, увидев LDA VAR1 предположит, что переменная "длиная". Встретив VAR1 EQU $28 присвоит ей значение $0028, а на LDA (VAR1),Y выдаст ошибку "требуется короткая переменная" (текст сообщения будет менее понятный, но суть не меняется).
Чтобы избежать этой ошибки, можно написать LDA (>VAR1),Y, но я не видел, чтобы этот приём кем-то использовался на Агате. Чаще подключение выполнялось на уровне бинарного файла. Но это тоже плохо:
1) Нужно примерно знать свободный регион ОЗУ для библиотеки и компилировать её, указав этот адрес.
2) В основной программе нужно повторить все описания меток. Замечу, что ДОК не имел инструкции .include.
Этих проблем вроде бы не много, но, фактически, они приводили к тому, что любая - крупная ли, мелкая - библиотеки всякий раз дорабатывались напильником под конкретную прогу.
Под библиотекой не обязательно понимать нечто логически обособленное.
Представьте себе обычный агатовский ДОС 3.3. Сколько в нём сегментов?
1) Таки нулевая страница, размазанная между ячейками IOSub и сисмона.
2) Автолоадер первого этапа: код сектора 0/0, загружающийся ПЗУшным лоадером при старте машины.
3) RWTS (драйвер дисковода), загружающийся автолоадером первого этапа.
4) Автолоадер второго этапа: код в нескольких секторах от 0/1, загружающийся автолоадером первого этапа.
5) Остальная кодовая тушка, которую с секторов 2/4, вниз по номерам, грузит автолоадер второго этапа.
6) Регион массивов десктрипторов файлов (где-то от адреса $9600). Не загружается, инициализируется при старте системы.
7) Регион ключевых векторов, размещается где-то на адресах $3C0..$3FF, инициализируется при старте системы.
Как скомпилировать такую программенцию ? Только по частям. Примерно прикидывая размещение всех частей. Как собрать в кучу? Как правило, на агате использовали командный файл такого вида:
[LOAD MAIN,1000
[LOAD SPRITE,2800
[LOAD FONT,3000
[SAVE PROGA,1000,2800
Это в совсем простом случае.
Развитый механизм сегментов как раз таки призван решать этот вопрос.
Возвращаемся к библиотекам L1 и L2. Используя ca65 можно сделать так:
Текст L1:
.zeropage
var1 .res 1
.code
lda var1
Текст L2:
.zeropage
var2 .res 1
.code
lda var2
В чём смысл: сегмент нулевой страницы zeropage заполняется самостоятельно, отдельно от сегмента кода code. В результате мы получим размещение var2 сразу за var1, а lda var2 сразу за lda var1. Если мы объявим, что zeropage начинается с адреса $A7, а code с адреса $2800, то var1 = $A7, var2 = $A8, а lda var1 размещена по адресу $2800, lda var2 по адресу $2802.
Тексты библиотек, таким образом, никак не связаны, но память выделяется аккуратно, байт за байтом, причем без нашего участия. Более того, это будет происходить как в случае объединения библиотек на уровне текстов (например, используя .include или просто сделав copy-paste), так и при сборке линкером уже скомпилированных объектных файлов.
Идём дальше. Вы можете объявить в программе несколько кодовых сегментов:
.loader_code, адрес загрузки = $1000
<тут размещён конфигуратор программы>
.main_code, адрес запуска = $d000
<тут основная программа>
Причём линкер создаёт описание фактически получившихся сегментов, которые вы можете использовать внутри программы. Например, вы можете в конфигураторе программы разместить процедуру копирования main_code в ЭмПЗУ. Линкер в этом случае настроит loader_code для работы по адресу $1000, а main_code для работы по адресу $d000 и затем разместит их подряд в один файл. Причем в loader_code будет передана информация как об адресе загрузки, так и об адресе запуска main_code и, конечно, фактическом размере main_code.
Это я немного упрощённо показал структуру файла HELLO, в котором живёт Basic-60.
Только вот в Basic-60 всё это собиралось и подгонялось вручную.
Стандартные названия сегментов (они стары как весь компьютерный мир и до сих пор применяются) такие:
.code (или чаще .text) - основная часть кода программы;
.data - данные, которые должны быть определены до запуска программы (что-то вроде static int a=10);
.rodata - то же, что .data, но запрещена модификация данных;
.bss - данные, которые могут быть неопределены (или обнулены) до запуска программы;
.eeprom - данные, хранимые в энергонезависимой памяти (часто встречается у микроконтроллеров).
Логично предположить, что раз данные в сегменте bss - просто нули, в файл его записывать не нужно. С другой стороны - память всё таки нужно выделить. А если по каким-то причинам нам нужно несколько сегментов кода или данных? Мы плавно подошли к вопросу о возникновении формата EXE-файлов. Если помните, в MS-DOS были популярны файлы COM (практически аналог агатовских B- и К- типов). Отличие EXE- файлов от COM состоит как раз таки именно в том, что EXE как бы не до конца слинкован: т.е. в нём есть отдельные секции (сегменты) кода, данных и чего угодно ещё.
И операционка, загружая такой файл, выделяет память для каждого сегмента в соответствии с его описанием в EXE. И это одинакого относится как к MS-DOS, так и к Windows (там структура EXE была усложнена, в частности добавлены ссылки на требуемые библиотеки, тип операционной системы, для которой предназначена прога и т.д.).
У Агата в операционках не предусмотрена линковка при загрузке, поэтому собирать сегменты в кучу - задача (в случае использования cc65) линкера ld65. И так как параметров-правил сборки немало, он использует отдельный конфиг-файл для их хранения. Например, я создал файл конфига для сборки программ, запускающихся в среде ДОК.
А как насчёт заголовка FIL-контейнера ?
В конфиге линкера описываем сегмент размером $28 байт с именем HEADER и правило интеграции в программу: "перед другими сегментами". А исходник сегмента описываем в include-файле, который подключаем к основной программе. Так как описание идёт в отдельном сегменте, оно никак не влияет на код программы и на её данные. Но так, как заголовок собирается наравне с другими сегментами, при его генерации линкер может подставить фактические значения точки загрузки и длины кода (а также данных, если объявлен сегменты .data и .rodata).
На сегодня всё. Дальше я планирую сделать несколько типовых конфигураций линкера (по крайней мере для Бейсик-60 и ИКП-Бейсик) и HEADER-файлы для синтаза B- FIL. Наверное, можно надрессировать ld65 и на генерацию S- файлов BTK или плагинов Basic Master.
Ставьте лайки, подписывайтесь на канал :)