Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Электроника МС 0511 (УКНЦ)

разработка на С

GCC кросскомпиляция

Собираю здесь все что связано с разработкой на C, предпочтительно с использованием кросскомпиляции GCC

  1. В папке gcc лежит скрипт build_gcc_uknc.sh с помощью которого можно собрать тулчейн для сборки проектов для УКНЦ.

  2. В папке examples лежат минимальные примеры приложений, собираемых с помощью тулчейна, получаемого скриптом из пункта 1.

  3. В папке libs лежит libppu — библиотека для загрузки и запуска кода в ПП (периферийном процессоре) со стороны ЦПУ (ppuc_alloc/ppuc_load_code/ppuc_run и т.п. — префикс ppuc_ у клиентских функций отличает их от серверных, которые работают в самом ПП; см. libs/libppu/ppu_client.h). Собирается и устанавливается в тулчейн тем же скриптом из пункта 1 — libppu.a/ppu_client.h оказываются рядом с libc.a, так что достаточно -lppu при линковке. Как собрать и загрузить код для самого ПП — см. раздел ниже.

Компиляция и линковка одной командой

Благодаря STARTFILE_SPEC и полноценной стандартной библиотеке (newlib, см. ниже) отдельный Makefile не обязателен — gcc сам скомпилирует и слинкует программу за один вызов, с printf/malloc/atoi и остальным stdio.h/ stdlib.h из коробки:

pdp11-uknc-rt11-gcc -std=gnu23 -fomit-frame-pointer -O2 \
  -Wl,--gc-sections -o example.sav example.c

В результате получается готовый .sav файл — самостоятельный образ памяти в формате RT-11 SAV, который можно сразу записывать на диск (rt11dsk) и запускать на УКНЦ под RT-11 без какой-либо дополнительной обработки.

Целевой триплет — pdp11-uknc-rt11. Тулчейн (binutils + GCC + newlib) собирается из ванильных исходников (binutils-2.45, gcc-15.2.0, newlib снапшот, см. раздел newlib ниже) с наложением патчей из форков wdigger/binutils-gdb, wdigger/gcc и wdigger/sourceware-mirror-newlib-cygwinbuild_gcc_uknc.sh скачивает их автоматически. Ниже — что именно эти патчи меняют относительно ванильных binutils/GCC/newlib.

Сборка и запуск кода на ПП (libppu)

Программа для ПП (периферийного процессора) — это не обычный .sav, а отдельный relocatable-модуль в родном формате RT-11 (.ppu), который ЦПУ-программа загружает в память ПП и запускает во время своей работы, а не то, что грузит и запускает сама RT-11.

Со стороны ПП (это как раз код, который будет реально выполняться на периферийном процессоре) достаточно определить точку входа ppu_main — аналог main для обычных программ:

#include "ppu_server.h"

void ppu_main(void) {
  ppus_debug_print(" PPU IS ALIVE");
}

Настоящую точку входа (START — обязательное имя по конвенции RT-11/PRUN) и обвязку вокруг неё (вызов ppu_main, затем автоматический выход через ppus_exit()) даёт стартовый код из самой libppu (libs/libppu/ppus_start.c) — писать это самому не нужно. ppus_debug_print() печатает строку через EMT 056 резидентного отладочного монитора ПП; ppus_exit() можно вызвать и самому, если нужен ранний выход, не дожидаясь возврата из ppu_main.

Собирается .ppu-файл специальным линкером pdp11-uknc-rt11-ld-ppu (тонкая обёртка над pdp11-uknc-rt11-ld, устанавливается тем же скриптом из пункта 1 вместе с libppu) — она уже прячет внутри и нужный линкер-скрипт (ppu.ld, гарантирующий, что START всегда попадает на смещение 0 — то есть не зависит от того, в каком порядке файлы перечислены в командной строке), и линковку с libppu.a:

pdp11-uknc-rt11-gcc -c -O2 pptest.c -o pptest.o
pdp11-uknc-rt11-ld-ppu -o pptest.ppu pptest.o

Со стороны ЦПУ загрузка и запуск — через ppuc_load_code()/ppuc_run() из ppu_client.h:

#include "ppu_client.h"

long ppu_addr = ppuc_load_code("PPTEST.PPU");
if (ppu_addr >= 0) {
  ppuc_run((unsigned short) ppu_addr);
}

ppuc_load_code() сам читает файл с диска RT-11, разбирает блоки GSD/TXT/RLD/ENDMOD, раскладывает .text/.data/.bss по памяти ПП, применяет релокации и возвращает адрес загрузки — он же адрес точки входа (гарантия ppu.ld выше избавляет от отдельного поиска символа START в самом загрузчике). Рабочий пример целиком — examples/ppurun (ЦПУ-часть ppurun.c загружает и запускает pptest.ppu, собранный из pptest.c).

Обмен данными между уже запущенными ЦПУ- и ПП-программами — поверх ppuc_load_code()/ppuc_run() выше, для программы, которая уже работает на ПП (а не только загружается/стартует), есть два независимых канала передачи произвольных буферов, оба поверх CPU↔PPU-каналов, независимых от канала 2, который использует резидентный монитор:

  • ЦПУ → ПП: ppuc_send() (ppu_client.h) на стороне ЦПУ, ppus_recv_init(callback) на стороне ПП (ppu_server.h) — программа для ПП вызывает ppus_recv_init() один раз из ppu_main(), передавая ей свой обработчик (void (*)(const void *buf, unsigned int size), вызывается из прерывания на каждый принятый буфер); ppuc_send() можно вызывать многократно, её первый вызов после каждого ppuc_run() дожидается подтверждения от ppus_recv_init(), что тот реально перехватил канал у резидентного монитора (иначе первые несколько байт мог бы получить не он).
  • ПП → ЦПУ: ppus_send() (ppu_server.h) на стороне ПП, на стороне ЦПУ — на выбор: простой блокирующий ppuc_recv(), либо (тем же вектором 0460, что и у ПП свой 0340) ppuc_recv_init(callback) — interrupt-driven приём, не блокирующий выполнение между сообщениями. У обоих одно и то же условие: если программа на ПП также использует ppus_recv_init()/ppuc_send(), на ЦПУ первым обязательно должен быть вызван ppuc_send() (см. выше), иначе служебный байт подтверждения будет спутан с началом настоящего сообщения — ppuc_recv_init() нужно вызывать уже после этого первого ppuc_send(), не раньше. И симметрично ppus_recv_shutdown() ниже: если использовали ppuc_recv_init(), перед выходом программы обязателен парный ppuc_recv_shutdown().

Оба канала используют один и тот же формат — little-endian 16-битная длина, затем сами байты; подробности и точные гарантии — в комментариях ppu_client.h/ppu_server.h. Сама библиотека внутри буфера смысл байтов не разбирает — если сообщения бывают разных видов (не только данные), это на совести программы: рабочий пример обоих каналов сразу — examples/ppupong (циклический обмен "ping N"/"pong N"), где каждое сообщение начинается с байта типа (MSG_TYPE_DATA/MSG_TYPE_EXIT), а не только с самих данных — так ПП-программа отличает обычный пинг от явной команды на выход из своего цикла и возврат управления резидентному монитору. Важно: если ПП-программа использует ppus_recv_init(), она обязана перед выходом из ppu_main() вызвать ppus_recv_shutdown() — иначе вектор прерывания остаётся указывать на память, которую ppus_exit() тут же освобождает, и монитор ПП зависает при первом же последующем прерывании канала 2. Тот же риск и в обратную сторону: ЦПУ-программа, использовавшая ppuc_recv_init(), обязана вызвать ppuc_recv_shutdown() перед своим .EXIT — RT-11 память между программами не очищает, так что не восстановленный вектор 0460 после завершения программы будет указывать в память, которую перезапишет следующая загруженная программа.

Реальный ввод с клавиатуры и захват экранаexamples/gfour (три прыгающих цветных квадрата на экране, до первого нажатия любой клавиши): ПП-часть (gfourppu.c) строит собственный тег-лист в plane 0 поверх штатного текстового экрана RT-11 и ставит свой обработчик прерывания на вектор 0300 — единственный реальный путь к клавиатуре (0177700/0177702), который есть только у ПП; канал 0 (тот, что использует .TTYIN) для этого не годится — он просто перестаёт обслуживаться, как только тег-лист захвачен. Каждое нажатие ПП пересылает ЦПУ через ppus_send()/ppuc_recv_init() (канал 1, см. выше), а по первому же настоящему нажатию (не отпусканию) сама восстанавливает то, что подменила — вектор 0300, исходный тег 0 и очищенный экран — и только после этого возвращает управление резидентному монитору ПП, чтобы консоль RT-11 действительно ожила снова, а не осталась зависшей картинкой. Подробности и почему это не сработало бы через канал 0 — в комментариях gfourppu.c/gfour.c.

binutils

  • sav-pdp11 (bfd/sav-pdp11.c) — новый write-only BFD-таргет, который пишет исполняемые файлы RT-11 SAV напрямую, без внешнего конвейера bin2load/lda2sav. Заголовок SAV содержит стартовый адрес, начальный SP (адрес вершины пользовательской памяти, а не точки входа — RT-11 копирует это слово в адрес 042 перед передачей управления) и битовую карту занятых блоков.

  • Вендор uknc (config.sub) — алиас для pdp11-uknc-aout; при сборке GCC для этого вендора автоматически включается -mbm2 (проц 1801ВМ2), если пользователь не указал -mbm1/-mbm2 явно.

  • Система rt11 (config.sub, gas/configure.tgt, bfd/config.bfd, ld/configure.tgt) — отдельная (от aout) система в триплете (pdp11-uknc-rt11). Формат sav-pdp11 компилируется и виден только для неё (через -DBFD_SUPPORTS_SAV_PDP11 в targ_cflags); aout остаётся побайтово ванильным binutils.

  • Встроенный линкер-скрипт (ld/emulparams/pdp11rt11.sh, ld/scripttempl/pdp11rt11.sc) — эмуляция pdp11rt11 со скриптом, зашитым в ld, так что -T больше не требуется.

  • pdp11rt11sav — эмуляция по умолчанию для rt11ld для этой системы сразу производит .sav. Технически линковка всё равно идёт через a.out-pdp11 (у него есть собственный, проверенный код разрешения релокаций — pdp11_aout_link_input_section), а конвертация в SAV происходит уже после того, как файл полностью записан на диск, через новый необязательный хук эмуляции ldafter_close_output (ld/ldemul.h/.c, ld/emultempl/emulation.em, один вызов в ld/ldmain.c). Так получилось после того, как выяснилось, что прямая линковка в sav-pdp11 через generic BFD final-link даёт неверные релокации (например, для строковых констант).

  • pdp11rt11rel — альтернативный формат вывода: родной relocatable-объект RT-11 (ld/emultempl/pdp11rt11rel.em, ld/emulparams/pdp11rt11rel.sh) — вместо a.out-pdp11 пишет собственный формат RT-11 "Relocatable Object Language" (блоки GSD/TXT/RLD/ENDMOD, имена в RADIX-50), описанный в RT-11 Volume and File Formats Manual — то, что понимает штатный линкер RT-11. Устройство то же, что у pdp11rt11sav: линковка идёт через -r в a.out-pdp11 (тот же проверенный pdp11_aout_link_input_section), а готовый файл затем перечитывается обычными вызовами BFD (bfd_canonicalize_symtab/reloc) и переупаковывается в формат RT-11 — без отдельного BFD-таргета в обе стороны. Нужна именно -r:

    pdp11-uknc-rt11-gcc -c -O2 foo.c -o foo.o
    pdp11-uknc-rt11-ld -r -m pdp11rt11rel foo.o -o foo.rel

    Символы длиннее 6 символов и непредставимые в RADIX-50 (например, _ в начале имени, которое добавляет сам компилятор) усекаются/заменяются на . — ограничение самого формата 1970-х, а не этой реализации.

GCC

  • Поддержка процессоров 1801ВМ1/1801ВМ2 (-mbm1/-mbm2) — Советские клоны PDP-11, отличия в системе команд от штатных моделей DEC.
  • Исправление кодогенерации и использование инструкции SOB для организации циклов сдвига — по мелочи улучшают качество генерируемого кода под эти процессоры.
  • Исправление реального бага компилятора: 32×32-битное знаковое умножение (mulhisi3) компилировалось неверно (ошибка в reload/LRA) — найдено и исправлено на реальном тестовом коде.
  • -fstack-usage/-Wstack-usage — бэкенд pdp11 раньше молча игнорировал эти опции; теперь размер стека кадра репортится, как и на других таргетах.
  • Система rt11 в триплете (gcc/config.gcc) — pdp11-uknc-rt11 зарегистрирован как отдельная (от pdp11-uknc-aout) система; задел под будущий порт newlib (у него configure.host выбирает libc/sys/<os> по системе триплета, а aout для этого не подходит — это формат объектных файлов, а не среда выполнения).
  • STARTFILE_SPEC для rt11 (gcc/config/pdp11/pdp11.h, libgcc/config.host, libgcc/config/pdp11/{t-pdp11rt11,crt0rt.s, parse_args.c}) — обычная линковка для pdp11-uknc-rt11 работает без -nostartfiles и ручной линковки crt0rt.o: crt0rt.o/parse_args.o собираются как часть сборки libgcc (по образцу libgcc/config/nds32/t-nds32) и подключаются автоматически. crt0rt.s берёт SP из заголовка SAV-файла и строит argc/argv из области командной строки, которую RT-11 сама заполняет при запуске программы (argv[0] — пустая строка-плейсхолдер: RT-11 никогда не сообщает программе её собственное имя). pdp11-uknc-aout эту особенность не получает и продолжает использовать штатный STARTFILE_SPEC GCC.
  • -msoft-float по умолчанию для вендора uknc (gcc/common/config/pdp11/pdp11-common.cc) — настоящее железо УКНЦ не имеет сопроцессора плавающей точки, а штатный дефолт pdp11-таргета (TARGET_DEFAULT_TARGET_FLAGS) рассчитан на 11/45 с аппаратным FPU, из-за чего -msoft-float раньше приходилось указывать вручную в каждой сборке. Теперь это дефолт для вендора ukncrt11, и aout); -mfpu по-прежнему работает как явное переопределение, а для остальных pdp11-таргетов поведение не меняется.
  • Исправление второго реального бага компилятора: ICE в проходе cprop_hardreg (internal compiler error: in extract_constrain_insn, at recog.cc:2783) на обычном коде вида for (;; --a) { if (*a) break; *a = 1; }. Причина — cbranch<mode>4 в pdp11.md делит один паттерн на все режимы QI/HI/SI/DI через слишком широкий предикат general_operand, хотя широкие (SI/DI) сравнения эмулируются cmpsi/cmpdi через раздельное чтение адреса дважды (по слову) — с автоинкрементной/автодекрементной адресацией это в принципе небезопасно, и cmpsi/cmpdi сами это запрещают через свои ограничения, но cbranch<mode>4 пропускал такую адресацию до прохода auto_inc_dec, который сворачивал декремент прямо в адрес сравнения. Исправлено новым предикатом cmp_operand (gcc/config/pdp11/predicates.md), который отклоняет side-effect-адреса именно для SImode/DImode — однословные (HI/QI) сравнения, где такая адресация легитимна, не затронуты.

newlib

Минимальный порт newlib для pdp11-uknc-rt11 (форк wdigger/sourceware-mirror-newlib-cygwin, ветка rt11-port) — даёт обычный printf/malloc/atoi/файловый I/O вместо того, чтобы каждый пример писал свою крошечную libc с нуля.

  • libc/sys/rt11/syscalls.c_read/_write/_open/_close/ _lseek/_fstat/_isatty/_sbrk/_exit, плюс ENOSYS-заглушки для _kill/_getpid/_link/_unlink/_stat. Консольный ввод-вывод (fd 0/1/2) идёт через RT-11's .TTYIN/.TTYOUT (EMT), значение явно закреплено за регистром R0 через register ... asm("r0") — обычного ограничения "r" недостаточно, EMT читает/пишет именно R0. _sbrk растит кучу от _end (символ линкера) вверх, с ограничением по текущему SP и запасом. Стартовый код (crt0rt.o/parse_args.o) сюда не входит — он приходит из GCC's STARTFILE_SPEC (см. раздел GCC выше), поэтому configure.host ставит have_crt0="no" для этой системы.
  • configure.host: sys_dir=rt11 для pdp11-uknc-rt11; отдельная ветка pdp11* в CPU-диспетчере не даёт провалиться в дефолтный кейс, который ставит -DMISSING_SYSCALL_NAMES (это подменило бы _read/_write/и т.д. на голые имена без подчёркивания, которых этот порт не определяет — используется обычная конвенция с подчёркиванием, как у d10v).
  • Флаги сборки (build_gcc_uknc.sh): --enable-newlib-nano-malloc, --enable-newlib-nano-formatted-io, --disable-newlib-wide-orient — компактные malloc/printf и отключение поддержки wchar_t в stdio (она здесь не нужна). Без этого printf в простой программе легко перевешивает за 60KB — критично на 64KB адресном пространстве PDP-11.
  • configure/Makefile.in пересобираются autoreconf с закреплёнными версиями autoconf 2.69 и automake 1.15.1 (build_gcc_uknc.sh сам их скачивает и собирает) — именно теми, которыми newlib сама была сгенерирована. Более новый automake меняет схему префиксов имён объектных файлов, из-за чего собственный жёстко прописанный список MATHOBJS_IN_LIBC в newlib/Makefile.am перестаёт совпадать с реальными именами, и libc.a не собирается («no entry ... in archive» при ar).
  • Два реальных бага компилятора нашлись и исправлены при разработке порта (см. раздел GCC выше, не workaround — реальные патчи в wdigger/gcc): reload-ICE на объединении double с меньшим целым в union (обходится -msoft-float, теперь дефолт для uknc) и cprop_hardreg-ICE на 32-битном сравнении с автодекрементом (libc/stdlib/gdtoa-hexnan.c — единственное место в newlib, что на это напоролось; исправлено в бэкенде новым предикатом cmp_operand).

Ограничения файлового ввода-вывода (libc/sys/rt11/syscalls.c):

Ограничения, продиктованные системой:

  1. Размер файла известен только с точностью до блока (512 байт) — для файла, который эта сессия не писала (чтение, либо переоткрытие после close).
  2. RADIX-50 имя: 3/6/3 символа (устройство/имя/расширение) — жёсткий лимит самой RT-11; если имя длиннее, выдаётся ошибка ENAMETOOLONG.
  3. Монитор RT-11 SJ синхронный — .LOOKUP/.ENTER/.READW/.WRITW блокируют вызывающую программу, никакого queued/асинхронного I/O.

Осознанно принятые ограничения:

  1. Работают только уже резидентные устройства (SY:, TT:, MQ:, UB:, PI:) — .FETCH не вызывается.
  2. O_APPEND побайтово точен только в рамках одной непрерывно открытой сессии fd; после close+reopen — точность до блока, выделение не растёт за пределы исходного .LOOKUP.

Открытая проблема:

  1. Смешивание puts()/printf с реальным файловым I/O иногда приводит к тихому обрыву программы; локализовано до места (после write(), не породившего ни одного .WRITW), причина предположительно в самом мониторе RT-11 или в эмуляции, не в GCC.

Известные ограничения

  • Бэкенд pdp11 не поддерживает global constructors (__attribute__ ((constructor)), а также конструкторы статических C++ объектов — но C++ и так не включён, тулчейн собирается с --enable-languages=c). При попытке их использовать компилятор выдаёт sorry, unimplemented: global constructors not supported on this target. Практическое следствие — -fstack-protector не работает: кодогенерация проверки канарейки вставляется как обычно, но линковка падает с undefined reference to __stack_chk_guard/__stack_chk_fail, а не понятной ошибкой на этапе компиляции (newlib не может собрать код, который инициализирует __stack_chk_guard, — именно из-за отсутствия constructors). Это старое ограничение бэкенда, не что-то, привнесённое этим проектом; чинить не планируется, поскольку ничего в проекте пока не требует ни -fstack-protector, ни constructors как таковых.
  • Бэкенд pdp11 не поддерживает weak-символы — компилятор явно предупреждает: warning: weak declaration of 'foo' not supported (проверено на изолированном минимальном примере). Практическое следствие: newlib's _printf_float специально объявлен weak, чтобы dtoa/mprec (арифметика произвольной точности для форматирования float, ~10KB кода) линковались только при реальном использовании %f/%e/%g — но на этом таргете weak-ссылка становится обычной сильной, и dtoa/mprec подтягиваются при любом вызове printf, даже если в форматной строке только %d/%ld. Для программ, которым важен размер (см. spigot в examples), единственный обходной путь — не использовать printf вовсе (свой ltoa/itoa + fputs/putchar). Тоже старое ограничение формата/бэкенда, не баг newlib-порта.
  • Бэкенд pdp11 не поддерживает -ffunction-sections/-fdata-sections — компилятор явно предупреждает: warning: '-ffunction-sections' not supported for this target (и то же для -fdata-sections), флаги молча игнорируются. Судя по всему, из-за формата a.out — фиксированный набор секций .text/.data/.bss, а не произвольные именованные секции, нужные --gc-sections, чтобы выбрасывать отдельные неиспользуемые функции/переменные. Из-за этого -Wl,--gc-sections в examples/*/Makefile реально помогает только на уровне целых объектных файлов библиотек (что линковщик и так делает при линковке из .a), а не внутри пользовательского .c-файла.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages