1 Отредактировано Voldemar0 (18-02-2018 15:13)

Тема: Пакет cc65

Привет!

Предлагаю тут вести обсуждение пакета cc65.

Чем полезен:

Готовый мультиплатформенный кросс-транслятор для mos6502.
Компиляет с C, макро-ассемблера, ведёт объектные файлы, библиотеки и даже имеет дизассемблер.

Читать много полезного тут:
http://www.cc65.org/doc/

Предполагаю, что добавлением простого препроцессора/обёртки можно заставить его компилять проги, предназначенные для ДОК и получать на выходе FIL-файлы либо сразу закидывать результат в образ.

Пример компиляции простой проги:

        .setcpu "6502"

pCOUT   := $36

        ldx #0
loop:   lda msg, x
        beq fin
        jsr cout
        inx
        bne loop

fin:    ;rts
        jmp ($fffc)     ; вектор сброса

msg:    .asciiz "Hello world"

cout:   jmp (pCOUT)

Этот текст пишем в PC-текстовом редакторе.
Затем компилируем (пакет cc65 где-то в путях либо в текущем каталоге либо указываем к нему полный путь):

ca65 -l -v -v -v hw.asm
ld65 -o hw.bin -t none -S '$1800' hw.o

На выходе получаем hw.bin
Я согнал эти команды в командный файл, чтобы не набирать каждый раз.
Закидываем сюда же загрузочный образ Агата с бейсиком-60 (для ИКП надо будет больше действий), запускаем dos33c2 и заходим в образ.
Нажимаем F9, выбираем "импорт файла"/"Двоичный B-файл, без перекодировки"
и набираем имя файла "hw.bin".
Файл импортируется, для удобства его можно переименовать (нажмите букву "r") в "HW".
Затем загружаемся с этого образа в эмуляторе (ну или на реальном Агате) и набираем "BRUN HW":

]BRUN HWhелло ворлд
]_       

Кодировка неправильная, это надо решать. Есть разные варианты, cc65 поддерживает внутренние виды перекодировок, но агата там, понятно, нет.
Но в остальном код вполне исправный.

2

Re: Пакет cc65

Правильная тема! Вот только на днях на этот проект наткнулся.
Меня, правда, больше интересует именно C-шная составляющая. Потому что 30-40 Кб исходников на асме еще как-то можно пережить, но дальше - закопаешься с головой. А на C код намного чище и понятнее.

3 Отредактировано Voldemar0 (19-02-2018 06:04)

Re: Пакет cc65

Alex начал этой темой заниматься, но, говорит- код очень тяжелый генеряет. Я попробовал - подтверждаю.
Там создаётся собственный стек, сложение, даже на 8 бит, через кучу операций проходит.

Есть Aztec-C, портированный на агат, если есть интерес - покопай его, опишешь.

4 Отредактировано Voldemar0 (24-02-2018 20:49)

Re: Пакет cc65

Прочитал полностью доку на ca65, потестил для режима сборки библиотека+программа - рулез.
Обёртку вокруг этого ассемблера делать ни к чему - много благостного будет потеряно. Надо только допилить импортёр готового бинарника в образ (уже есть работающие наброски).
С перекодировкой строк разобрался: у него это хотя и немного странно (нужно на каждый символ написать отдельную команду), но поддерживается внутри.

Мелкие недостатки:
- Очень странно, что символы > < (взятие младшего/старшего байта от 16-битного значения) используются наоборот, чем в ДОК.
- Также точки в именах переменных не допускаются (можно разрешить только точку в начале имени).
- И не разрешены комментарии, начинающиеся со "*".

Есть несколько нейтральных отличий. В основном, это замена ключевых слов DFB, ASC, DW, EQU и прочих на свои аналоги. EQU разделён на два варианта: "=" (для обычных значений) и ":=" (для адресов/указателей). Как я понял, отличие только в дампе информации для какого нибудь внешнего отладчика.

Прочие отличия положительны:
1) Есть поддержка сегментов, есть готовый сегмент нулевой страницы. Т.е. можно тупо забивать переменные в Segment "zeropage" (в разных частях проги и библиотек) и линкер затем разложит их по порядку. Пока не разобрался, как сделать сегмент совсем сквозным, но это надо вчитаться теперь в описание линкера.

2) При экспорте/импорте переменных чётко отслеживается размер - один или два байта. Линкер всё проверяет. Т.е. если я пишу, что ожидаю здесь переменную ZP, то и связанные с ней команды будут собираться с короткой адресацией, даже если фактическое значение ещё неизвестно.

3) Есть локальные метки, начинаются с "@":

IOS_PRINT_PTR:
 STX IOS_PTR+0
 STA IOS_PTR+1
 LDY #0

@loop:
 LDA (IOS_PTR),Y
 BEQ @fin
 JSR IOS_COUT
 INY
 BNE @loop

@fin:
 JSR IOS_CR
 SEC
 RTS

Их можно использовать многократно, главное, чтобы между ними стояли нормальные метки (это граница локальности).

4) Поддержка комментариев /* в стиле C */

5) Нет команды DCI , зато есть ASCIIZ. Для нынешней ситуации с повальным увлечением полными знакогенераторами так даже лучше.

6) Есть встроенная переменная .time - можно вкомпиливать в прогу дату сборки (и выводить потом где нибудь).

 year = .time / 31556926 + 1970
 year_m = .time .mod 31556926
 month = year_m / 2629743 + 1
 month_m = year_m .mod 2629743
 day = month_m / 86400 + 1
 
 jsr IOS_PRINT
 .asciiz .sprintf("Сборка программы: %02d.%02d.%04d", day, month, year)

7) Мощная арифметика констант. Не инженерный калькулятор, но хотя бы умножение/деление есть. Все переменные 32-битные. Старшие 16 разрядов могут использоваться, например, как номер банка памяти.

Там ещё полно плюшек, но, IMHO, для агатовских задач они излишни.

Ну и просто быстро это всё. Мои примеры собираются от исходника до bin меньше чем за секунду.

5 Отредактировано Voldemar0 (26-02-2018 06:03)

Re: Пакет cc65

Сегодня мне удалось добиться от 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.


Ставьте лайки, подписывайтесь на канал :)

6 Отредактировано Voldemar0 (04-03-2018 14:13)

Re: Пакет cc65

Начал готовить демку для cc65 под винду: никогда такого не было и вот опять нашелся баг :(

Суть в том, что когда-то кому-то пришло в голову сделать в языке C символьный тип знаковым. Через это символ с кодом $FF удачно преобразуется в -1, строго равную специальному коду EOF - конец файла.
Эта проблема есть как в source-base unix-версии, так и в официальном бинаре под винду.

Т.е. если ca65 встречает в тексте программы (хотя бы даже в комментарии) символ "я" - обработка текста прерывается на этом месте или не идёт дальше данной строки.

Под юнихами, с их православной КОИ8-r это ерунда, потому что $FF - очень сильно нужный заглавный твёрдый знак ("Ъ"). А вот под виндой это маленькая "я". Заметно полезная.

Лечение очень простое: в исходнике ca65 (cc65 этому не подвержен) нужно поправить строку.

cc65-2.13.2/src/ca65/scanner.c

Тут описание структуры struct InputFile. Меняем в ней поле

unsigned char   Line[256];          /* The current input line */

и всё начинает работать нормально.


Если вы владеете искусством сборки C-шных прог под винду, попробуйте пересобрать ca65 с этой правкой или попробуйте разобраться, кто сейчас является владельцем официального репозитория cc65 и опишите ему этот баг.

7 Отредактировано Voldemar0 (08-03-2018 17:48)

Re: Пакет cc65

Сделал пример проги из темы про линкер, но теперь перенесённой на ca65.

Распаковываем аттач куда нибудь, например, c:\exm-c65\.
Создаём там же каталог c:\exm-c65\cc65.
Пакет самого cc65 качаем с их сайта: http://www.cc65.org/
Распаковываем его в c:\exm-c65\cc65.

hw.bat - вызов компилятора и редактора связей для сборки примера. Запускаем его.
Смотрим на сообщения об ошибках. Думаем, исправляем.

Затем, используя dos33c2, закидываем получившийся hw.fil в ikp_7.dsk

Запускаем образ в эмуляторе.

Выбираем в меню ИКП "Ассемблер"

После загрузки и появления цифры "1" нажимаем ESC, выбираем "выйти в отладчик", там пишем:

[RUN HELLOW

Наслаждаемся.

Файлы в аттаче:

hw.asm - пример простой проги, классом чуть выше чем "Hello World!"
IOSUB.asm - пример простой библиотеки, которая используется программой hw.asm и компилируется отдельно.

agatfeat.inc - Рекомендуется для включения во все исходники. Немного подстраивает ca65 под ДОК-style.

agatfilK.inc
agatfilB.inc - Синтезирует K или B- заголовки для FIL-файлов.
    Без одного из этих файлов получится обычный бинарь (.bin).
    Даже если сделаете расширение .FIL, dos33c2 будет показывать в таком файле мусор.

chars_koi.inc
chars_win.inc - Перекодирует все строковые константы в агатовскую кодировку из вашей (cp1251 или КОИ8-R).
    Этот файл пристёгивается к вашей программе через agatfeat.inc.

agat-B-IKP.cfg
agat-B-basic60.cfg
agat-K-DOK.cfg - Любой из этих файлов указывается линкеру (ld65) в параметре -C
    и описывает рекомендуемую карту памяти.


!! Важно: Пример IOSUB.asm / HW.asm для некоторого упрощения пилился только под ДОК,
так что собирать его под другие ОС нет смысла без доработки !!


*.o - это объектные файлы транслятора, бинарники, смотреть ни к чему.
*.map - карта памяти, генерируется редактором связей, текст, смотреть интересно.
*.lst - листинг, представляет собой текст программы и комментарии транслятора. Смотреть крайне полезно. Позволяет понять, как транслятор видит ваши исходники.


Неочевидные из примера, но важные особенности:

   - знаки > и < используются наоборот: > - старший байт, < - младший байт
   - точки в именах переменных не допускаются (можно разрешить только точку в начале имени)
   - не разрешены комментарии, начинающиеся со "*"
   - допускаются HEX-числа как с префиксом $ так и с суффиксом h
   - в исходнике, даже в комментариях, недопустим символ с кодом $FF - транслятор считает его концом файла

Post's attachments

Attachment icon cc65-complex-example.zip 112.45 kb, 60 downloads since 2018-03-08